@dxos/app-framework 0.6.5 → 0.6.6-main.e1a6e1f

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.
@@ -4,9 +4,13 @@
4
4
 
5
5
  import { z } from 'zod';
6
6
 
7
+ import { S } from '@dxos/echo-schema';
8
+
7
9
  import type { IntentData } from '../IntentPlugin';
8
10
  import type { Plugin } from '../PluginHost';
9
11
 
12
+ //
13
+ // --- Constants --------------------------------------------------------------
10
14
  // NOTE(thure): These are chosen from RFC 1738’s `safe` characters: http://www.faqs.org/rfcs/rfc1738.html
11
15
  export const SLUG_LIST_SEPARATOR = '+';
12
16
  export const SLUG_ENTRY_SEPARATOR = '_';
@@ -15,93 +19,130 @@ export const SLUG_PATH_SEPARATOR = '~';
15
19
  export const SLUG_COLLECTION_INDICATOR = '';
16
20
  export const SLUG_SOLO_INDICATOR = '$';
17
21
 
18
- export const parseSlug = (slug: string): { id: string; path: string[]; solo: boolean } => {
19
- const solo = slug.startsWith(SLUG_SOLO_INDICATOR);
20
- const cleanSlug = solo ? slug.replace(SLUG_SOLO_INDICATOR, '') : slug;
21
- const [id, ...path] = cleanSlug.split(SLUG_PATH_SEPARATOR);
22
-
23
- return { id, path, solo };
24
- };
25
-
26
22
  //
27
- // Provides
28
- //
29
-
23
+ // --- Types ------------------------------------------------------------------
24
+ const LayoutEntrySchema = S.mutable(
25
+ S.Struct({
26
+ id: S.String,
27
+ solo: S.optional(S.Boolean),
28
+ path: S.optional(S.String),
29
+ }),
30
+ );
31
+
32
+ export type LayoutEntry = S.Schema.Type<typeof LayoutEntrySchema>;
33
+
34
+ // TODO(Zan): Consider making solo it's own part. It's not really a function of the 'main' part?
35
+ // TODO(Zan): Consider renaming the 'main' part to 'deck' part now that we are throwing out the old layout plugin.
36
+ // TODO(Zan): Extend to all strings?
37
+ const LayoutPartSchema = S.Union(
38
+ S.Literal('sidebar'),
39
+ S.Literal('main'),
40
+ S.Literal('complementary'),
41
+ S.Literal('fullScreen'),
42
+ );
43
+ export type LayoutPart = S.Schema.Type<typeof LayoutPartSchema>;
44
+
45
+ const LayoutPartsSchema = S.partial(S.mutable(S.Record(LayoutPartSchema, S.mutable(S.Array(LayoutEntrySchema)))));
46
+ export type LayoutParts = S.Schema.Type<typeof LayoutPartsSchema>;
47
+
48
+ const LayoutCoordinateSchema = S.mutable(S.Struct({ part: LayoutPartSchema, entryId: S.String }));
49
+ export type LayoutCoordinate = S.Schema.Type<typeof LayoutCoordinateSchema>;
50
+
51
+ const PartAdjustmentSchema = S.Union(S.Literal('increment-start'), S.Literal('increment-end'), S.Literal('solo'));
52
+ export type PartAdjustment = S.Schema.Type<typeof PartAdjustmentSchema>;
53
+
54
+ const LayoutAdjustmentSchema = S.mutable(
55
+ S.Struct({ layoutCoordinate: LayoutCoordinateSchema, type: PartAdjustmentSchema }),
56
+ );
57
+ export type LayoutAdjustment = S.Schema.Type<typeof LayoutAdjustmentSchema>;
58
+
59
+ /** @deprecated */
30
60
  export const ActiveParts = z.record(z.string(), z.union([z.string(), z.array(z.string())]));
61
+ export type ActiveParts = z.infer<typeof ActiveParts>;
31
62
 
32
63
  /**
33
64
  * Basic state provided by a navigation plugin.
34
65
  */
35
- // TODO(wittjosiah): Replace Zod w/ Effect Schema to align with ECHO.
36
- // TODO(wittjosiah): We should align this more with `window.location` along the lines of what React Router does.
37
- export const Location = z.object({
38
- active: z
39
- .union([z.string(), ActiveParts])
40
- .optional()
41
- .describe('Id of currently active item, or record of item id(s) keyed by the app part in which they are active.'),
42
- closed: z
43
- .union([z.string(), z.array(z.string())])
44
- .optional()
45
- .describe('Id or ids of recently closed items, in order of when they were closed.'),
46
- });
47
-
48
66
  export const Attention = z.object({
49
67
  attended: z.set(z.string()).optional().describe('Ids of items which have focus.'),
50
68
  });
51
-
52
- export type ActiveParts = z.infer<typeof ActiveParts>;
53
- export type Location = z.infer<typeof Location>;
54
69
  export type Attention = z.infer<typeof Attention>;
55
70
 
56
- // QUESTION(Zan): Is fullscreen a part? Or a special case of 'main'?
57
- export type LayoutPart = 'sidebar' | 'main' | 'complementary';
58
-
59
- export type LayoutCoordinate = { part: LayoutPart; index: number; partSize: number; solo?: boolean };
60
- export type NavigationAdjustmentType = `${'pin' | 'increment'}-${'start' | 'end'}`;
61
- export type NavigationAdjustment = { layoutCoordinate: LayoutCoordinate; type: NavigationAdjustmentType };
62
-
63
- export const isActiveParts = (active: string | ActiveParts | undefined): active is ActiveParts =>
64
- !!active && typeof active !== 'string';
65
-
66
- export const isAdjustTransaction = (data: IntentData | undefined): data is NavigationAdjustment =>
67
- !!data &&
68
- ('layoutCoordinate' satisfies keyof NavigationAdjustment) in data &&
69
- ('type' satisfies keyof NavigationAdjustment) in data;
70
-
71
- export const firstMainId = (active: Location['active']): string =>
72
- isActiveParts(active) ? (Array.isArray(active.main) ? active.main[0] : active.main) : active ?? '';
73
-
74
- export const activeIds = (active: string | ActiveParts | undefined): Set<string> =>
75
- active
76
- ? isActiveParts(active)
77
- ? Object.values(active).reduce((acc, ids) => {
78
- Array.isArray(ids) ? ids.forEach((id) => acc.add(id)) : acc.add(ids);
79
- return acc;
80
- }, new Set<string>())
81
- : new Set([active])
82
- : new Set();
83
-
84
- export const isIdActive = (active: string | ActiveParts | undefined, id: string): boolean => {
85
- return active
86
- ? isActiveParts(active)
87
- ? Object.values(active).findIndex((ids) => (Array.isArray(ids) ? ids.indexOf(id) > -1 : ids === id)) > -1
88
- : active === id
89
- : false;
90
- };
91
-
92
71
  /**
93
72
  * Provides for a plugin that can manage the app navigation.
94
73
  */
95
- export type LocationProvides = {
96
- location: Readonly<Location>;
97
- };
74
+ const LocationProvidesSchema = S.mutable(
75
+ S.Struct({
76
+ location: S.Struct({
77
+ active: LayoutPartsSchema,
78
+ closed: S.Array(S.String),
79
+ }),
80
+ }),
81
+ );
82
+ export type LocationProvides = S.Schema.Type<typeof LocationProvidesSchema>;
98
83
 
99
84
  /**
100
85
  * Type guard for layout plugins.
101
86
  */
102
- export const parseNavigationPlugin = (plugin: Plugin) => {
103
- const { success } = Location.safeParse((plugin.provides as any).location);
104
- return success ? (plugin as Plugin<LocationProvides>) : undefined;
87
+ export const isLayoutParts = (value: unknown): value is LayoutParts => {
88
+ return S.is(LayoutPartsSchema)(value);
89
+ };
90
+
91
+ // Type guard for PartAdjustment
92
+ export const isLayoutAdjustment = (value: unknown): value is LayoutAdjustment => {
93
+ return S.is(LayoutAdjustmentSchema)(value);
94
+ };
95
+
96
+ export const parseNavigationPlugin = (plugin: Plugin): Plugin<LocationProvides> | undefined => {
97
+ const location = (plugin.provides as any)?.location;
98
+ if (!location) {
99
+ return undefined;
100
+ }
101
+
102
+ if (S.is(LocationProvidesSchema)({ location })) {
103
+ return plugin as Plugin<LocationProvides>;
104
+ }
105
+
106
+ return undefined;
107
+ };
108
+
109
+ /**
110
+ * Utilities.
111
+ */
112
+
113
+ /** Extracts all unique IDs from the layout parts. */
114
+ export const openIds = (layout: LayoutParts): string[] => {
115
+ return Object.values(layout)
116
+ .flatMap((part) => part?.map((entry) => entry.id) ?? [])
117
+ .filter((id): id is string => id !== undefined);
118
+ };
119
+
120
+ export const firstIdInPart = (layout: LayoutParts | undefined, part: LayoutPart): string | undefined => {
121
+ if (!layout) {
122
+ return undefined;
123
+ }
124
+
125
+ return layout[part]?.at(0)?.id;
126
+ };
127
+
128
+ export const indexInPart = (
129
+ layout: LayoutParts | undefined,
130
+ layoutCoordinate: LayoutCoordinate | undefined,
131
+ ): number | undefined => {
132
+ if (!layout || !layoutCoordinate) {
133
+ return undefined;
134
+ }
135
+
136
+ const { part, entryId } = layoutCoordinate;
137
+ return layout[part]?.findIndex((entry) => entry.id === entryId);
138
+ };
139
+
140
+ export const partLength = (layout: LayoutParts | undefined, part: LayoutPart | undefined): number => {
141
+ if (!layout || !part) {
142
+ return 0;
143
+ }
144
+
145
+ return layout[part]?.length ?? 0;
105
146
  };
106
147
 
107
148
  //
@@ -115,6 +156,7 @@ export enum NavigationAction {
115
156
  SET = `${NAVIGATION_ACTION}/set`,
116
157
  ADJUST = `${NAVIGATION_ACTION}/adjust`,
117
158
  CLOSE = `${NAVIGATION_ACTION}/close`,
159
+ EXPOSE = `${NAVIGATION_ACTION}/expose`,
118
160
  }
119
161
 
120
162
  /**
@@ -129,9 +171,11 @@ export namespace NavigationAction {
129
171
  * Payload for adding an item to the active items.
130
172
  */
131
173
  export type AddToActive = IntentData<{
174
+ part: LayoutPart;
132
175
  id: string;
133
176
  scrollIntoView?: boolean;
134
- pivot?: { id: string; position: 'add-before' | 'add-after' };
177
+ pivotId?: string;
178
+ positioning?: 'start' | 'end';
135
179
  }>;
136
180
  /**
137
181
  * A subtractive overlay to apply to `location.active` (i.e. the result is a subtraction from the previous active of the argument)
@@ -144,5 +188,5 @@ export namespace NavigationAction {
144
188
  /**
145
189
  * An atomic transaction to apply to `location.active`, describing which element to (attempt to) move to which location.
146
190
  */
147
- export type Adjust = IntentData<NavigationAdjustment>;
191
+ export type Adjust = IntentData<LayoutAdjustment>;
148
192
  }