@dxos/app-framework 0.6.5 → 0.6.6-staging.582ce24

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,104 +4,139 @@
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 = '_';
13
17
  export const SLUG_KEY_VALUE_SEPARATOR = '-';
14
18
  export const SLUG_PATH_SEPARATOR = '~';
15
19
  export const SLUG_COLLECTION_INDICATOR = '';
16
- export const SLUG_SOLO_INDICATOR = '$';
17
-
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
20
 
26
21
  //
27
- // Provides
28
- //
29
-
22
+ // --- Types ------------------------------------------------------------------
23
+ const LayoutEntrySchema = S.mutable(S.Struct({ id: S.String, path: S.optional(S.String) }));
24
+
25
+ export type LayoutEntry = S.Schema.Type<typeof LayoutEntrySchema>;
26
+
27
+ // TODO(Zan): Consider making solo it's own part. It's not really a function of the 'main' part?
28
+ // TODO(Zan): Consider renaming the 'main' part to 'deck' part now that we are throwing out the old layout plugin.
29
+ // TODO(Zan): Extend to all strings?
30
+ const LayoutPartSchema = S.Union(
31
+ S.Literal('sidebar'),
32
+ S.Literal('main'),
33
+ S.Literal('solo'),
34
+ S.Literal('complementary'),
35
+ S.Literal('fullScreen'),
36
+ );
37
+ export type LayoutPart = S.Schema.Type<typeof LayoutPartSchema>;
38
+
39
+ const LayoutPartsSchema = S.partial(S.mutable(S.Record(LayoutPartSchema, S.mutable(S.Array(LayoutEntrySchema)))));
40
+ export type LayoutParts = S.Schema.Type<typeof LayoutPartsSchema>;
41
+
42
+ const LayoutCoordinateSchema = S.mutable(S.Struct({ part: LayoutPartSchema, entryId: S.String }));
43
+ export type LayoutCoordinate = S.Schema.Type<typeof LayoutCoordinateSchema>;
44
+
45
+ const PartAdjustmentSchema = S.Union(S.Literal('increment-start'), S.Literal('increment-end'), S.Literal('solo'));
46
+ export type PartAdjustment = S.Schema.Type<typeof PartAdjustmentSchema>;
47
+
48
+ const LayoutAdjustmentSchema = S.mutable(
49
+ S.Struct({ layoutCoordinate: LayoutCoordinateSchema, type: PartAdjustmentSchema }),
50
+ );
51
+ export type LayoutAdjustment = S.Schema.Type<typeof LayoutAdjustmentSchema>;
52
+
53
+ /** @deprecated */
30
54
  export const ActiveParts = z.record(z.string(), z.union([z.string(), z.array(z.string())]));
55
+ export type ActiveParts = z.infer<typeof ActiveParts>;
31
56
 
32
57
  /**
33
58
  * Basic state provided by a navigation plugin.
34
59
  */
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
60
  export const Attention = z.object({
49
61
  attended: z.set(z.string()).optional().describe('Ids of items which have focus.'),
50
62
  });
51
-
52
- export type ActiveParts = z.infer<typeof ActiveParts>;
53
- export type Location = z.infer<typeof Location>;
54
63
  export type Attention = z.infer<typeof Attention>;
55
64
 
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
65
  /**
93
66
  * Provides for a plugin that can manage the app navigation.
94
67
  */
95
- export type LocationProvides = {
96
- location: Readonly<Location>;
97
- };
68
+ const LocationProvidesSchema = S.mutable(
69
+ S.Struct({
70
+ location: S.Struct({
71
+ active: LayoutPartsSchema,
72
+ closed: S.Array(S.String),
73
+ }),
74
+ }),
75
+ );
76
+ export type LocationProvides = S.Schema.Type<typeof LocationProvidesSchema>;
98
77
 
99
78
  /**
100
79
  * Type guard for layout plugins.
101
80
  */
102
- export const parseNavigationPlugin = (plugin: Plugin) => {
103
- const { success } = Location.safeParse((plugin.provides as any).location);
104
- return success ? (plugin as Plugin<LocationProvides>) : undefined;
81
+ export const isLayoutParts = (value: unknown): value is LayoutParts => {
82
+ return S.is(LayoutPartsSchema)(value);
83
+ };
84
+
85
+ // Type guard for PartAdjustment
86
+ export const isLayoutAdjustment = (value: unknown): value is LayoutAdjustment => {
87
+ return S.is(LayoutAdjustmentSchema)(value);
88
+ };
89
+
90
+ export const parseNavigationPlugin = (plugin: Plugin): Plugin<LocationProvides> | undefined => {
91
+ const location = (plugin.provides as any)?.location;
92
+ if (!location) {
93
+ return undefined;
94
+ }
95
+
96
+ if (S.is(LocationProvidesSchema)({ location })) {
97
+ return plugin as Plugin<LocationProvides>;
98
+ }
99
+
100
+ return undefined;
101
+ };
102
+
103
+ /**
104
+ * Utilities.
105
+ */
106
+
107
+ /** Extracts all unique IDs from the layout parts. */
108
+ export const openIds = (layout: LayoutParts): string[] => {
109
+ return Object.values(layout)
110
+ .flatMap((part) => part?.map((entry) => entry.id) ?? [])
111
+ .filter((id): id is string => id !== undefined);
112
+ };
113
+
114
+ export const firstIdInPart = (layout: LayoutParts | undefined, part: LayoutPart): string | undefined => {
115
+ if (!layout) {
116
+ return undefined;
117
+ }
118
+
119
+ return layout[part]?.at(0)?.id;
120
+ };
121
+
122
+ export const indexInPart = (
123
+ layout: LayoutParts | undefined,
124
+ layoutCoordinate: LayoutCoordinate | undefined,
125
+ ): number | undefined => {
126
+ if (!layout || !layoutCoordinate) {
127
+ return undefined;
128
+ }
129
+
130
+ const { part, entryId } = layoutCoordinate;
131
+ return layout[part]?.findIndex((entry) => entry.id === entryId);
132
+ };
133
+
134
+ export const partLength = (layout: LayoutParts | undefined, part: LayoutPart | undefined): number => {
135
+ if (!layout || !part) {
136
+ return 0;
137
+ }
138
+
139
+ return layout[part]?.length ?? 0;
105
140
  };
106
141
 
107
142
  //
@@ -115,6 +150,7 @@ export enum NavigationAction {
115
150
  SET = `${NAVIGATION_ACTION}/set`,
116
151
  ADJUST = `${NAVIGATION_ACTION}/adjust`,
117
152
  CLOSE = `${NAVIGATION_ACTION}/close`,
153
+ EXPOSE = `${NAVIGATION_ACTION}/expose`,
118
154
  }
119
155
 
120
156
  /**
@@ -129,14 +165,16 @@ export namespace NavigationAction {
129
165
  * Payload for adding an item to the active items.
130
166
  */
131
167
  export type AddToActive = IntentData<{
168
+ part: LayoutPart;
132
169
  id: string;
133
170
  scrollIntoView?: boolean;
134
- pivot?: { id: string; position: 'add-before' | 'add-after' };
171
+ pivotId?: string;
172
+ positioning?: 'start' | 'end';
135
173
  }>;
136
174
  /**
137
175
  * A subtractive overlay to apply to `location.active` (i.e. the result is a subtraction from the previous active of the argument)
138
176
  */
139
- export type Close = IntentData<{ activeParts: ActiveParts }>;
177
+ export type Close = IntentData<{ activeParts: ActiveParts; noToggle?: boolean }>;
140
178
  /**
141
179
  * The active parts to directly set, to be used when working with URLs or restoring a specific state.
142
180
  */
@@ -144,5 +182,5 @@ export namespace NavigationAction {
144
182
  /**
145
183
  * An atomic transaction to apply to `location.active`, describing which element to (attempt to) move to which location.
146
184
  */
147
- export type Adjust = IntentData<NavigationAdjustment>;
185
+ export type Adjust = IntentData<LayoutAdjustment>;
148
186
  }