@trypattern/sdk 0.1.9 → 0.1.11
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/README.md +37 -8
- package/dist/app.d.ts +1 -0
- package/dist/app.js +4 -3
- package/dist/cli/app.js +119 -6
- package/dist/cli/publishing.js +2 -0
- package/dist/components/AppFrame.d.ts +4 -1
- package/dist/components/AppFrame.js +25 -7
- package/dist/components/Card.d.ts +7 -2
- package/dist/components/Card.js +59 -3
- package/dist/components/Checkbox.d.ts +10 -0
- package/dist/components/Checkbox.js +59 -0
- package/dist/components/Choice.d.ts +15 -0
- package/dist/components/Choice.js +80 -0
- package/dist/components/ChoiceGroup.d.ts +11 -0
- package/dist/components/ChoiceGroup.js +44 -0
- package/dist/components/Icon.d.ts +2 -1
- package/dist/components/Icon.js +2 -2
- package/dist/components/Image.d.ts +11 -0
- package/dist/components/Image.js +76 -0
- package/dist/components/ListRow.d.ts +3 -1
- package/dist/components/ListRow.js +18 -4
- package/dist/components/PlainScreen.d.ts +16 -0
- package/dist/components/PlainScreen.js +80 -0
- package/dist/components/PlainSidebar.d.ts +15 -0
- package/dist/components/PlainSidebar.js +179 -0
- package/dist/components/Screen.d.ts +3 -1
- package/dist/components/Screen.js +5 -1
- package/dist/components/ScrollArea.d.ts +2 -1
- package/dist/components/ScrollArea.js +7 -2
- package/dist/components/Text.d.ts +1 -1
- package/dist/components/Text.js +10 -4
- package/dist/components/TextField.d.ts +5 -1
- package/dist/components/TextField.js +47 -9
- package/dist/components/choiceContext.d.ts +4 -0
- package/dist/components/choiceContext.js +7 -0
- package/dist/components/frame.d.ts +2 -0
- package/dist/components/icons.d.ts +3 -0
- package/dist/components/icons.js +4 -1
- package/dist/components/interaction.d.ts +1 -1
- package/dist/entityStore.d.ts +1 -0
- package/dist/entityStore.js +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/media.d.ts +36 -0
- package/dist/media.js +74 -0
- package/dist/publishWire.d.ts +2 -0
- package/dist/publishWire.js +21 -1
- package/dist/react.d.ts +5 -0
- package/dist/react.js +4 -0
- package/dist/rules/checkApiRouting.js +1 -1
- package/dist/rules/lintPlugin.js +13 -4
- package/dist/rules/messages.d.ts +1 -0
- package/dist/rules/messages.js +1 -0
- package/dist/schema.d.ts +2 -0
- package/dist/schema.js +1 -0
- package/dist/server/createServer.d.ts +5 -3
- package/dist/server/createServer.js +32 -2
- package/dist/server/env.d.ts +3 -1
- package/dist/server/imageHeader.d.ts +10 -0
- package/dist/server/imageHeader.js +159 -0
- package/dist/server/index.d.ts +6 -1
- package/dist/server/index.js +3 -0
- package/dist/server/jsonLd.d.ts +45 -0
- package/dist/server/jsonLd.js +88 -0
- package/dist/server/media.d.ts +16 -0
- package/dist/server/media.js +135 -0
- package/dist/server/pages.d.ts +72 -0
- package/dist/server/pages.js +301 -0
- package/dist/server/session.js +1 -1
- package/dist/server/tables.d.ts +3 -0
- package/dist/server/tables.js +23 -8
- package/dist/start.js +30 -2
- package/dist/table.d.ts +1 -0
- package/dist/table.js +8 -1
- package/dist/tables.d.ts +2 -1
- package/dist/tables.js +10 -1
- package/package.json +1 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import * as stylex from '@stylexjs/stylex';
|
|
3
|
-
export function ScrollArea({ children, style }) {
|
|
4
|
-
return _jsx("div", { ...stylex.props(styles.scroll, style), children: children });
|
|
3
|
+
export function ScrollArea({ children, style, axis = 'vertical' }) {
|
|
4
|
+
return (_jsx("div", { ...stylex.props(styles.scroll, axis === 'horizontal' && styles.horizontal, style), children: children }));
|
|
5
5
|
}
|
|
6
6
|
const styles = stylex.create({
|
|
7
7
|
scroll: {
|
|
@@ -9,5 +9,10 @@ const styles = stylex.create({
|
|
|
9
9
|
minHeight: 0,
|
|
10
10
|
overflowY: 'auto',
|
|
11
11
|
overscrollBehavior: 'contain'
|
|
12
|
+
},
|
|
13
|
+
horizontal: {
|
|
14
|
+
height: 'auto',
|
|
15
|
+
overflowX: 'auto',
|
|
16
|
+
overflowY: 'hidden'
|
|
12
17
|
}
|
|
13
18
|
});
|
|
@@ -3,7 +3,7 @@ import type { Placement } from './Placement.ts';
|
|
|
3
3
|
import type { Tone } from './tones.ts';
|
|
4
4
|
type TextProps = {
|
|
5
5
|
children: ReactNode;
|
|
6
|
-
variant?: 'heading' | 'subheading' | 'title' | 'label' | 'body' | 'caption' | 'code';
|
|
6
|
+
variant?: 'heading' | 'subheading' | 'title' | 'lead' | 'label' | 'body' | 'caption' | 'code';
|
|
7
7
|
tone?: Tone;
|
|
8
8
|
align?: 'start' | 'center' | 'end';
|
|
9
9
|
as?: 'span' | 'p' | 'h1' | 'h2' | 'h3';
|
package/dist/components/Text.js
CHANGED
|
@@ -23,17 +23,23 @@ const styles = stylex.create({
|
|
|
23
23
|
const variants = stylex.create({
|
|
24
24
|
heading: {
|
|
25
25
|
fontSize: fontSizes.heading5,
|
|
26
|
-
fontWeight: fontWeights.
|
|
27
|
-
lineHeight: lineHeights.heading5
|
|
26
|
+
fontWeight: fontWeights.medium,
|
|
27
|
+
lineHeight: lineHeights.heading5,
|
|
28
|
+
letterSpacing: '-0.01em'
|
|
28
29
|
},
|
|
29
30
|
subheading: {
|
|
30
31
|
fontSize: fontSizes.heading6,
|
|
31
|
-
fontWeight: fontWeights.
|
|
32
|
+
fontWeight: fontWeights.medium,
|
|
32
33
|
lineHeight: lineHeights.heading6
|
|
33
34
|
},
|
|
34
35
|
title: {
|
|
35
36
|
fontSize: fontSizes.bodyBase,
|
|
36
|
-
fontWeight: fontWeights.
|
|
37
|
+
fontWeight: fontWeights.medium,
|
|
38
|
+
lineHeight: lineHeights.body
|
|
39
|
+
},
|
|
40
|
+
lead: {
|
|
41
|
+
fontSize: fontSizes.bodyBase,
|
|
42
|
+
fontWeight: fontWeights.normal,
|
|
37
43
|
lineHeight: lineHeights.body
|
|
38
44
|
},
|
|
39
45
|
label: {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { IconName } from './icons.ts';
|
|
1
2
|
import type { Placement } from './Placement.ts';
|
|
2
3
|
import type { Size } from './Size.ts';
|
|
3
4
|
type TextFieldProps = {
|
|
@@ -18,8 +19,11 @@ type TextFieldProps = {
|
|
|
18
19
|
inputMode?: 'text' | 'email' | 'numeric' | 'decimal' | 'tel' | 'url' | 'search';
|
|
19
20
|
maxLength?: number;
|
|
20
21
|
pattern?: string;
|
|
22
|
+
multiline?: boolean;
|
|
23
|
+
rows?: number;
|
|
21
24
|
size?: Size;
|
|
25
|
+
icon?: IconName;
|
|
22
26
|
style?: Placement;
|
|
23
27
|
};
|
|
24
|
-
export declare function TextField({ label, isLabelHidden, description, error, onChange, size, style, ...input }: TextFieldProps): import("react").JSX.Element;
|
|
28
|
+
export declare function TextField({ label, isLabelHidden, description, error, onChange, multiline, rows, size, icon, style, ...input }: TextFieldProps): import("react").JSX.Element;
|
|
25
29
|
export {};
|
|
@@ -2,19 +2,20 @@ 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
|
+
import { Icon } from "./Icon.js";
|
|
6
7
|
import { controlSizes } from "./controlLook.js";
|
|
7
8
|
import { interact } from "./interaction.js";
|
|
8
|
-
export function TextField({ label, isLabelHidden = false, description, error, onChange, size = 'md', style, ...input }) {
|
|
9
|
+
export function TextField({ label, isLabelHidden = false, description, error, onChange, multiline = false, rows = 3, size = 'md', icon, style, ...input }) {
|
|
9
10
|
const isInvalid = error !== undefined;
|
|
10
11
|
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) => {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
12
|
+
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 })), _jsxs("div", { ...stylex.props(styles.control), children: [icon === undefined ? null : (_jsx("span", { ...stylex.props(styles.icon), children: _jsx(Icon, { name: icon, tone: "tertiary" }) })), _jsx(Field.Control, { ...input, ...(multiline ? { render: _jsx("textarea", { rows: rows }) } : {}), "aria-label": isLabelHidden ? label : undefined, onChange: (event) => onChange?.(event.currentTarget.value), onFocus: (event) => {
|
|
13
|
+
valueOnFocus.current = event.currentTarget.value;
|
|
14
|
+
}, onBlur: (event) => {
|
|
15
|
+
if (event.currentTarget.value !== valueOnFocus.current) {
|
|
16
|
+
interact({ primitive: 'TextField', event: 'change', label });
|
|
17
|
+
}
|
|
18
|
+
}, className: stylex.props(styles.input, controlSizes[size], multiline && styles.multiline, multiline && multilinePaddings[size], icon !== undefined && styles.withIcon, 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
19
|
}
|
|
19
20
|
const focusRing = `inset 0 0 0 ${borders.hairline} ${colors.brandBorderFocus}, 0 0 0 ${borders.thin} ${colors.brandBorderFocus}, ${shadows.default}`;
|
|
20
21
|
const invalidRing = `inset 0 0 0 ${borders.hairline} ${colors.dangerBorderDefault}, 0 0 0 ${borders.thin} ${colors.dangerBorderDefault}, ${shadows.default}`;
|
|
@@ -25,6 +26,22 @@ const styles = stylex.create({
|
|
|
25
26
|
gap: space.xs,
|
|
26
27
|
minWidth: 0
|
|
27
28
|
},
|
|
29
|
+
control: {
|
|
30
|
+
position: 'relative',
|
|
31
|
+
display: 'flex',
|
|
32
|
+
flexDirection: 'column'
|
|
33
|
+
},
|
|
34
|
+
icon: {
|
|
35
|
+
position: 'absolute',
|
|
36
|
+
insetBlock: 0,
|
|
37
|
+
insetInlineStart: space.sm,
|
|
38
|
+
display: 'flex',
|
|
39
|
+
alignItems: 'center',
|
|
40
|
+
pointerEvents: 'none'
|
|
41
|
+
},
|
|
42
|
+
withIcon: {
|
|
43
|
+
paddingInlineStart: `calc(${space.xl} + ${space.xs})`
|
|
44
|
+
},
|
|
28
45
|
label: {
|
|
29
46
|
fontWeight: fontWeights.medium,
|
|
30
47
|
color: colors.primaryForeground
|
|
@@ -49,6 +66,10 @@ const styles = stylex.create({
|
|
|
49
66
|
color: colors.tertiaryForeground
|
|
50
67
|
}
|
|
51
68
|
},
|
|
69
|
+
multiline: {
|
|
70
|
+
height: 'auto',
|
|
71
|
+
resize: 'vertical'
|
|
72
|
+
},
|
|
52
73
|
invalid: {
|
|
53
74
|
boxShadow: {
|
|
54
75
|
default: invalidRing,
|
|
@@ -75,3 +96,20 @@ const styles = stylex.create({
|
|
|
75
96
|
color: colors.dangerText
|
|
76
97
|
}
|
|
77
98
|
});
|
|
99
|
+
const multilinePaddings = stylex.create({
|
|
100
|
+
xs: {
|
|
101
|
+
paddingBlock: `calc((${controls.heightXs} - ${lineHeights.caption}) / 2)`
|
|
102
|
+
},
|
|
103
|
+
sm: {
|
|
104
|
+
paddingBlock: `calc((${controls.heightSm} - ${lineHeights.bodySm}) / 2)`
|
|
105
|
+
},
|
|
106
|
+
md: {
|
|
107
|
+
paddingBlock: `calc((${controls.heightMd} - ${lineHeights.bodySm}) / 2)`
|
|
108
|
+
},
|
|
109
|
+
lg: {
|
|
110
|
+
paddingBlock: `calc((${controls.heightLg} - ${lineHeights.bodySm}) / 2)`
|
|
111
|
+
},
|
|
112
|
+
xl: {
|
|
113
|
+
paddingBlock: `calc((${controls.heightXl} - ${lineHeights.bodySm}) / 2)`
|
|
114
|
+
}
|
|
115
|
+
});
|
|
@@ -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;
|
package/dist/components/icons.js
CHANGED
|
@@ -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;
|
package/dist/entityStore.d.ts
CHANGED
|
@@ -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'>;
|
package/dist/entityStore.js
CHANGED
|
@@ -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";
|
package/dist/media.d.ts
ADDED
|
@@ -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
|
+
}
|
package/dist/publishWire.d.ts
CHANGED
|
@@ -20,7 +20,9 @@ export declare const UPLOAD: {
|
|
|
20
20
|
readonly compatibilityDate: "compatibilityDate";
|
|
21
21
|
readonly sdkVersion: "sdkVersion";
|
|
22
22
|
readonly message: "message";
|
|
23
|
+
readonly runWorkerFirst: "runWorkerFirst";
|
|
23
24
|
};
|
|
25
|
+
export declare function workerFirstPaths(text: string | undefined): readonly string[];
|
|
24
26
|
export declare const PUBLISH_MESSAGE_LIMIT = 100;
|
|
25
27
|
export declare function publishMessage(text: string): string | null;
|
|
26
28
|
export declare const publishStep: z.ZodEnum<{
|
package/dist/publishWire.js
CHANGED
|
@@ -91,8 +91,28 @@ export const UPLOAD = {
|
|
|
91
91
|
source: 'source/',
|
|
92
92
|
compatibilityDate: 'compatibilityDate',
|
|
93
93
|
sdkVersion: 'sdkVersion',
|
|
94
|
-
message: 'message'
|
|
94
|
+
message: 'message',
|
|
95
|
+
runWorkerFirst: 'runWorkerFirst'
|
|
95
96
|
};
|
|
97
|
+
const WORKER_FIRST_PATH = /^!?\/[\w.~\-/*]{0,100}$/;
|
|
98
|
+
const WORKER_FIRST_LIMIT = 32;
|
|
99
|
+
const ALWAYS_WORKER_FIRST = ['/api/*', '/media/*'];
|
|
100
|
+
export function workerFirstPaths(text) {
|
|
101
|
+
let declared;
|
|
102
|
+
try {
|
|
103
|
+
declared = JSON.parse(text ?? '[]');
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
declared = [];
|
|
107
|
+
}
|
|
108
|
+
const listed = declared === true ? ['/*'] : declared;
|
|
109
|
+
const extra = (Array.isArray(listed) ? listed : [])
|
|
110
|
+
.slice(0, WORKER_FIRST_LIMIT)
|
|
111
|
+
.filter((path) => typeof path === 'string' &&
|
|
112
|
+
WORKER_FIRST_PATH.test(path) &&
|
|
113
|
+
!ALWAYS_WORKER_FIRST.some((kept) => path.startsWith(`!${kept.slice(0, -1)}`)));
|
|
114
|
+
return [...new Set([...ALWAYS_WORKER_FIRST, ...extra])];
|
|
115
|
+
}
|
|
96
116
|
export const PUBLISH_MESSAGE_LIMIT = 100;
|
|
97
117
|
export function publishMessage(text) {
|
|
98
118
|
const firstLine = (text.split('\n')[0] ?? '').replaceAll(/\s+/g, ' ').trim();
|
package/dist/react.d.ts
CHANGED
|
@@ -6,6 +6,9 @@ export { Avatar } from './components/Avatar.tsx';
|
|
|
6
6
|
export { Badge } from './components/Badge.tsx';
|
|
7
7
|
export { Button } from './components/Button.tsx';
|
|
8
8
|
export { Card } from './components/Card.tsx';
|
|
9
|
+
export { Checkbox } from './components/Checkbox.tsx';
|
|
10
|
+
export { Choice } from './components/Choice.tsx';
|
|
11
|
+
export { ChoiceGroup } from './components/ChoiceGroup.tsx';
|
|
9
12
|
export { CommandMenu } from './components/CommandMenu.tsx';
|
|
10
13
|
export { CommandPalette } from './components/CommandPalette.tsx';
|
|
11
14
|
export type { CommandMenuItem } from './components/CommandMenu.tsx';
|
|
@@ -18,6 +21,7 @@ export { EmptyState } from './components/EmptyState.tsx';
|
|
|
18
21
|
export { Form } from './components/Form.tsx';
|
|
19
22
|
export { Icon } from './components/Icon.tsx';
|
|
20
23
|
export { IconButton } from './components/IconButton.tsx';
|
|
24
|
+
export { Image } from './components/Image.tsx';
|
|
21
25
|
export type { IconName } from './components/icons.ts';
|
|
22
26
|
export { Kbd } from './components/Kbd.tsx';
|
|
23
27
|
export { Link } from './components/Link.tsx';
|
|
@@ -48,3 +52,4 @@ export { Text } from './components/Text.tsx';
|
|
|
48
52
|
export { TextField } from './components/TextField.tsx';
|
|
49
53
|
export { Toaster } from './components/Toaster.tsx';
|
|
50
54
|
export { Tooltip } from './components/Tooltip.tsx';
|
|
55
|
+
export type { MediaImage } from './media.ts';
|
package/dist/react.js
CHANGED
|
@@ -6,6 +6,9 @@ export { Avatar } from "./components/Avatar.js";
|
|
|
6
6
|
export { Badge } from "./components/Badge.js";
|
|
7
7
|
export { Button } from "./components/Button.js";
|
|
8
8
|
export { Card } from "./components/Card.js";
|
|
9
|
+
export { Checkbox } from "./components/Checkbox.js";
|
|
10
|
+
export { Choice } from "./components/Choice.js";
|
|
11
|
+
export { ChoiceGroup } from "./components/ChoiceGroup.js";
|
|
9
12
|
export { CommandMenu } from "./components/CommandMenu.js";
|
|
10
13
|
export { CommandPalette } from "./components/CommandPalette.js";
|
|
11
14
|
export { Content } from "./components/Content.js";
|
|
@@ -17,6 +20,7 @@ export { EmptyState } from "./components/EmptyState.js";
|
|
|
17
20
|
export { Form } from "./components/Form.js";
|
|
18
21
|
export { Icon } from "./components/Icon.js";
|
|
19
22
|
export { IconButton } from "./components/IconButton.js";
|
|
23
|
+
export { Image } from "./components/Image.js";
|
|
20
24
|
export { Kbd } from "./components/Kbd.js";
|
|
21
25
|
export { Link } from "./components/Link.js";
|
|
22
26
|
export { ListRow } from "./components/ListRow.js";
|
|
@@ -2,7 +2,7 @@ import { request } from 'node:http';
|
|
|
2
2
|
import { createServer as createNetServer } from 'node:net';
|
|
3
3
|
import { createServer, preview } from 'vite';
|
|
4
4
|
const API_ROUTES_REACH_THE_WORKER = 'Browser navigations to /api/* (pasted links, OAuth callbacks) must reach the Hono worker, not the single-page app. Cloudflare serves index.html to navigations without running the worker unless wrangler.jsonc keeps "run_worker_first": ["/api/*"] under "assets". Without it, an OAuth callback lands on the app and loops.';
|
|
5
|
-
const APP_ROUTES_SERVE_THE_APP = 'Browser navigations outside /api/ must serve the app (index.html) so deep links like /issues open the screen. Keep "not_found_handling": "single-page-application" in wrangler.jsonc and keep run_worker_first
|
|
5
|
+
const APP_ROUTES_SERVE_THE_APP = 'Browser navigations outside /api/ must serve the app (index.html) so deep links like /issues open the screen. Keep "not_found_handling": "single-page-application" in wrangler.jsonc and keep run_worker_first to /api/* (plus /media/*, and the public page paths, /sitemap.xml and /robots.txt for an app with pages).';
|
|
6
6
|
const DEV_SIGN_IN_ONLY_IN_DEV = 'POST /api/auth/dev-sign-in signs anyone in without Google, so it must exist in the dev server (app sign-in, flows and screenshots use it) and must not exist in the production build. createServer in @trypattern/sdk mounts it only under import.meta.env.DEV.';
|
|
7
7
|
const probes = [
|
|
8
8
|
{
|
package/dist/rules/lintPlugin.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ACTIONS_WHERE_MESSAGE, COMPONENT_FIRST_MESSAGE, COMPONENT_FILE_NAME_MESSAGE, DARK_MODE_IN_TOKENS_MESSAGE, DEV_ONLY_SIGN_IN_MESSAGE, EXPLICIT_ACCESS_MESSAGE, USE_BREAKPOINTS_MESSAGE, HOVER_USES_HIGHLIGHT_MESSAGE, NO_CLASSNAME_STRINGS_MESSAGE, NO_COMMENTS_MESSAGE, NO_INTERACTIVE_TRANSITION_MESSAGE, NO_POINTER_CURSOR_MESSAGE, NO_ROUTE_DATA_APIS_MESSAGE, ROUTES_UNDER_APP_LAYOUT_MESSAGE, SCROLL_VIEW_ONLY_MESSAGE, SELECTABLE_ONLY_MESSAGE, STATE_VIA_HOOKS_MESSAGE, MODEL_BY_NAME_MESSAGE, MODEL_MEMBERS_MESSAGE, PURE_ACTIONS_MESSAGE, PURE_GETTERS_MESSAGE, USE_MODEL_WHERE_MESSAGE, CLASSES_NOT_FACTORIES_MESSAGE, STORES_IN_APP_MESSAGE, TYPED_STYLE_PROP_MESSAGE, USE_COLOR_TOKEN_MESSAGE, USE_FONT_TOKEN_MESSAGE, USE_LINK_MESSAGE, USE_PRIMITIVES_MESSAGE, NO_WIDGET_ROLES_MESSAGE, NO_CLICK_ON_STATIC_MESSAGE, PRIMITIVE_NAMES_MESSAGE, USE_SPACE_TOKENS_MESSAGE, USE_SIZE_TOKENS_MESSAGE, TEXT_VIA_TEXT_MESSAGE, TEXT_ELEMENTS_MESSAGE, LOOSE_TEXT_MESSAGE, COMPOSE_IN_SCREENS_MESSAGE, NO_HOVER_IN_APP_MESSAGE } from "./messages.js";
|
|
1
|
+
import { ACTIONS_WHERE_MESSAGE, COMPONENT_FIRST_MESSAGE, COMPONENT_FILE_NAME_MESSAGE, DARK_MODE_IN_TOKENS_MESSAGE, DEV_ONLY_SIGN_IN_MESSAGE, EXPLICIT_ACCESS_MESSAGE, USE_BREAKPOINTS_MESSAGE, HOVER_USES_HIGHLIGHT_MESSAGE, NO_CLASSNAME_STRINGS_MESSAGE, NO_COMMENTS_MESSAGE, NO_INTERACTIVE_TRANSITION_MESSAGE, NO_POINTER_CURSOR_MESSAGE, NO_ROUTE_DATA_APIS_MESSAGE, ROUTES_UNDER_APP_LAYOUT_MESSAGE, SCROLL_VIEW_ONLY_MESSAGE, SELECTABLE_ONLY_MESSAGE, STATE_VIA_HOOKS_MESSAGE, MODEL_BY_NAME_MESSAGE, MODEL_MEMBERS_MESSAGE, PURE_ACTIONS_MESSAGE, PURE_GETTERS_MESSAGE, USE_MODEL_WHERE_MESSAGE, CLASSES_NOT_FACTORIES_MESSAGE, STORES_IN_APP_MESSAGE, TYPED_STYLE_PROP_MESSAGE, USE_COLOR_TOKEN_MESSAGE, USE_FONT_TOKEN_MESSAGE, USE_LINK_MESSAGE, USE_PRIMITIVES_MESSAGE, USE_IMAGE_MESSAGE, NO_WIDGET_ROLES_MESSAGE, NO_CLICK_ON_STATIC_MESSAGE, PRIMITIVE_NAMES_MESSAGE, USE_SPACE_TOKENS_MESSAGE, USE_SIZE_TOKENS_MESSAGE, TEXT_VIA_TEXT_MESSAGE, TEXT_ELEMENTS_MESSAGE, LOOSE_TEXT_MESSAGE, COMPOSE_IN_SCREENS_MESSAGE, NO_HOVER_IN_APP_MESSAGE } from "./messages.js";
|
|
2
2
|
const TOKENS_FILE = 'src/theme/tokens.stylex.ts';
|
|
3
3
|
const APP_THEME_FILE = 'src/theme/theme.ts';
|
|
4
4
|
const SCROLL_AREA_FILE = 'src/components/ScrollArea.tsx';
|
|
@@ -10,7 +10,8 @@ const PRIMITIVE_ELEMENTS = new Set([
|
|
|
10
10
|
'select',
|
|
11
11
|
'dialog',
|
|
12
12
|
'progress',
|
|
13
|
-
'meter'
|
|
13
|
+
'meter',
|
|
14
|
+
'img'
|
|
14
15
|
]);
|
|
15
16
|
const WIDGET_ROLES = new Set([
|
|
16
17
|
'button',
|
|
@@ -50,6 +51,9 @@ const PRIMITIVES = new Set([
|
|
|
50
51
|
'Badge',
|
|
51
52
|
'Button',
|
|
52
53
|
'Card',
|
|
54
|
+
'Checkbox',
|
|
55
|
+
'Choice',
|
|
56
|
+
'ChoiceGroup',
|
|
53
57
|
'CommandMenu',
|
|
54
58
|
'CommandPalette',
|
|
55
59
|
'Content',
|
|
@@ -61,6 +65,7 @@ const PRIMITIVES = new Set([
|
|
|
61
65
|
'Form',
|
|
62
66
|
'Icon',
|
|
63
67
|
'IconButton',
|
|
68
|
+
'Image',
|
|
64
69
|
'Kbd',
|
|
65
70
|
'Link',
|
|
66
71
|
'ListRow',
|
|
@@ -939,8 +944,12 @@ const usePrimitives = rule((context) => ({
|
|
|
939
944
|
JSXOpeningElement(node) {
|
|
940
945
|
if (!SRC_FILE.test(context.filename))
|
|
941
946
|
return;
|
|
942
|
-
|
|
943
|
-
|
|
947
|
+
const name = elementName(node) ?? '';
|
|
948
|
+
if (PRIMITIVE_ELEMENTS.has(name)) {
|
|
949
|
+
context.report({
|
|
950
|
+
loc: node.loc,
|
|
951
|
+
message: name === 'img' ? USE_IMAGE_MESSAGE : USE_PRIMITIVES_MESSAGE
|
|
952
|
+
});
|
|
944
953
|
}
|
|
945
954
|
}
|
|
946
955
|
}));
|
package/dist/rules/messages.d.ts
CHANGED
|
@@ -34,6 +34,7 @@ export declare const PURE_GETTERS_MESSAGE = "A getter works an answer out from t
|
|
|
34
34
|
export declare const ACTIONS_WHERE_MESSAGE = "Menu entries and shortcuts are actions, and actions live only in src/actions/: one file per thing they act on, `export const issueActions = defineActions(({ page, models }) => [action({ id: 'issues.next', label: 'Next issue', keys: ['j'], send: [{ type: 'issueDetail.openNext' }] })])`, listed in src/actions/index.ts. If a person can see it in a menu or press a key for it, it is an action; what it does is a command on a model, and an action only sends commands. Models never build actions.";
|
|
35
35
|
export declare const PURE_ACTIONS_MESSAGE = "An actions file works its entries out from `page` and `models` (each model's state and answers) and nothing else, because Pattern reruns it only when something it read changes, and flows compare the result. It never waits, never uses `this`, never reads the clock or randomness, and imports only types from src/models/, src/stores/ and src/components/. If an entry needs something a model does not answer yet, add a getter to that model.";
|
|
36
36
|
export declare const USE_PRIMITIVES_MESSAGE = "Write <Button>, <IconButton>, <TextField>, <Select>, <Switch> or <Dialog> from @trypattern/sdk/react instead of this element. The primitives carry the focus, keyboard, disabled, pending and error behaviour and the design system's look, so a screen only says what the control does. If no primitive fits, the gap is in the SDK; do not build around it here.";
|
|
37
|
+
export declare const USE_IMAGE_MESSAGE = "Write <Image image={photo} sizes=\"\u2026\" /> from @trypattern/sdk/react instead of <img>. Photos live in the media table (app media add photo.jpg --alt \"\u2026\"); Image reserves their size, picks the right width for the screen, shows a blurred placeholder while loading and a neutral block when one is missing. Never hard-code an image URL.";
|
|
37
38
|
export declare const NO_WIDGET_ROLES_MESSAGE = "This role rebuilds a primitive by hand. Use the component from @trypattern/sdk/react with that behaviour (Button, Switch, Menu, Tooltip, Dialog, Divider, CommandMenu): it already has the role, the keyboard handling and the focus management, and it will keep working when the SDK is upgraded.";
|
|
38
39
|
export declare const NO_CLICK_ON_STATIC_MESSAGE = "A click on a <div> or <span> cannot be reached from the keyboard or by a screen reader. Make it a <Button variant=\"transparent\"> or an <IconButton> from @trypattern/sdk/react, or a <Link> if it goes somewhere.";
|
|
39
40
|
export declare const PRIMITIVE_NAMES_MESSAGE = "{name} is a primitive in @trypattern/sdk/react; a second one with the same name would drift from it. Import the SDK's, and if yours adds something (a fixed icon, a confirm step) name it for what it does, like DeleteIssue{name}, and render the SDK {name} inside.";
|
package/dist/rules/messages.js
CHANGED
|
@@ -34,6 +34,7 @@ export const PURE_GETTERS_MESSAGE = 'A getter works an answer out from this.stat
|
|
|
34
34
|
export const ACTIONS_WHERE_MESSAGE = "Menu entries and shortcuts are actions, and actions live only in src/actions/: one file per thing they act on, `export const issueActions = defineActions(({ page, models }) => [action({ id: 'issues.next', label: 'Next issue', keys: ['j'], send: [{ type: 'issueDetail.openNext' }] })])`, listed in src/actions/index.ts. If a person can see it in a menu or press a key for it, it is an action; what it does is a command on a model, and an action only sends commands. Models never build actions.";
|
|
35
35
|
export const PURE_ACTIONS_MESSAGE = "An actions file works its entries out from `page` and `models` (each model's state and answers) and nothing else, because Pattern reruns it only when something it read changes, and flows compare the result. It never waits, never uses `this`, never reads the clock or randomness, and imports only types from src/models/, src/stores/ and src/components/. If an entry needs something a model does not answer yet, add a getter to that model.";
|
|
36
36
|
export const USE_PRIMITIVES_MESSAGE = "Write <Button>, <IconButton>, <TextField>, <Select>, <Switch> or <Dialog> from @trypattern/sdk/react instead of this element. The primitives carry the focus, keyboard, disabled, pending and error behaviour and the design system's look, so a screen only says what the control does. If no primitive fits, the gap is in the SDK; do not build around it here.";
|
|
37
|
+
export const USE_IMAGE_MESSAGE = 'Write <Image image={photo} sizes="…" /> from @trypattern/sdk/react instead of <img>. Photos live in the media table (app media add photo.jpg --alt "…"); Image reserves their size, picks the right width for the screen, shows a blurred placeholder while loading and a neutral block when one is missing. Never hard-code an image URL.';
|
|
37
38
|
export const NO_WIDGET_ROLES_MESSAGE = 'This role rebuilds a primitive by hand. Use the component from @trypattern/sdk/react with that behaviour (Button, Switch, Menu, Tooltip, Dialog, Divider, CommandMenu): it already has the role, the keyboard handling and the focus management, and it will keep working when the SDK is upgraded.';
|
|
38
39
|
export const NO_CLICK_ON_STATIC_MESSAGE = 'A click on a <div> or <span> cannot be reached from the keyboard or by a screen reader. Make it a <Button variant="transparent"> or an <IconButton> from @trypattern/sdk/react, or a <Link> if it goes somewhere.';
|
|
39
40
|
export const PRIMITIVE_NAMES_MESSAGE = "{name} is a primitive in @trypattern/sdk/react; a second one with the same name would drift from it. Import the SDK's, and if yours adds something (a fixed icon, a confirm step) name it for what it does, like DeleteIssue{name}, and render the SDK {name} inside.";
|
package/dist/schema.d.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
export { call, defineServices } from './services.ts';
|
|
2
2
|
export type { CallInput, CallReply, ServiceCall, ServiceMemory, Services } from './services.ts';
|
|
3
|
+
export { media } from './media.ts';
|
|
4
|
+
export type { Media, MediaImage } from './media.ts';
|
|
3
5
|
export { defineTables } from './table.ts';
|
|
4
6
|
export type { Access, Columns, Fields, Level, PublicRow, Row, Seed, Table, TableDefinition, TableOf, Tables, Value, Writes } from './table.ts';
|
package/dist/schema.js
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import { Hono } from 'hono';
|
|
2
2
|
import type { Tables } from '../table.ts';
|
|
3
3
|
import type { ServerBindings, ServerEnv } from './env.ts';
|
|
4
|
-
|
|
4
|
+
import type { Pages } from './pages.ts';
|
|
5
|
+
export declare function createServer<Group extends Tables, Bindings extends ServerBindings = ServerBindings>({ tables, admins, routes, pages }: {
|
|
5
6
|
tables: Group;
|
|
6
7
|
admins: readonly string[];
|
|
7
8
|
routes?: Hono<ServerEnv<Bindings>>;
|
|
8
|
-
|
|
9
|
+
pages?: Pages;
|
|
10
|
+
}): import("hono/hono-base").HonoBase<ServerEnv, import("hono/types").BlankSchema | import("hono/types").MergeSchemaPath<(((({
|
|
9
11
|
"/api/health": {
|
|
10
12
|
$get: {
|
|
11
13
|
input: {};
|
|
@@ -483,4 +485,4 @@ export declare function createServer<Group extends Tables, Bindings extends Serv
|
|
|
483
485
|
status: import("hono/utils/http-status").ContentfulStatusCode;
|
|
484
486
|
};
|
|
485
487
|
};
|
|
486
|
-
}, "/api/tables">, "/
|
|
488
|
+
}, "/api/tables">, "/">, "/", "/">;
|