@skillcdn/console 0.4.3 → 0.4.4
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 +4 -4
- package/dist/api.d.ts +3 -3
- package/dist/api.d.ts.map +1 -1
- package/dist/api.js +3 -3
- package/dist/api.js.map +1 -1
- package/dist/cli/cli.d.ts.map +1 -1
- package/dist/cli/cli.js +95 -20
- package/dist/cli/cli.js.map +1 -1
- package/dist/cli/format.d.ts +4 -2
- package/dist/cli/format.d.ts.map +1 -1
- package/dist/cli/format.js +32 -9
- package/dist/cli/format.js.map +1 -1
- package/dist/cli/help.d.ts.map +1 -1
- package/dist/cli/help.js +17 -14
- package/dist/cli/help.js.map +1 -1
- package/dist/cli/plugin-check.d.ts +5 -1
- package/dist/cli/plugin-check.d.ts.map +1 -1
- package/dist/cli/plugin-check.js +23 -4
- package/dist/cli/plugin-check.js.map +1 -1
- package/dist/cli/plugin-cli.d.ts.map +1 -1
- package/dist/cli/plugin-cli.js +10 -1
- package/dist/cli/plugin-cli.js.map +1 -1
- package/dist/cli/plugin-init.d.ts +2 -0
- package/dist/cli/plugin-init.d.ts.map +1 -1
- package/dist/cli/plugin-init.js +6 -0
- package/dist/cli/plugin-init.js.map +1 -1
- package/dist/client.d.ts +4 -0
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +2 -0
- package/dist/client.js.map +1 -1
- package/dist/compat.js +1 -1
- package/dist/components/board.d.ts +8 -0
- package/dist/components/board.d.ts.map +1 -1
- package/dist/components/board.js +3 -3
- package/dist/components/board.js.map +1 -1
- package/dist/components/decision-form.d.ts +6 -0
- package/dist/components/decision-form.d.ts.map +1 -1
- package/dist/components/decision-form.js +23 -5
- package/dist/components/decision-form.js.map +1 -1
- package/dist/components/decision-list.d.ts +5 -1
- package/dist/components/decision-list.d.ts.map +1 -1
- package/dist/components/decision-list.js +25 -5
- package/dist/components/decision-list.js.map +1 -1
- package/dist/components/event-feed.d.ts.map +1 -1
- package/dist/components/event-feed.js +7 -1
- package/dist/components/event-feed.js.map +1 -1
- package/dist/components/goals.d.ts.map +1 -1
- package/dist/components/goals.js +2 -2
- package/dist/components/goals.js.map +1 -1
- package/dist/components/home.d.ts +35 -0
- package/dist/components/home.d.ts.map +1 -0
- package/dist/components/home.js +326 -0
- package/dist/components/home.js.map +1 -0
- package/dist/components/pack-connect.d.ts.map +1 -1
- package/dist/components/pack-connect.js +5 -4
- package/dist/components/pack-connect.js.map +1 -1
- package/dist/components/people.d.ts +2 -0
- package/dist/components/people.d.ts.map +1 -1
- package/dist/components/people.js +15 -2
- package/dist/components/people.js.map +1 -1
- package/dist/components/plugin-frame.d.ts +3 -12
- package/dist/components/plugin-frame.d.ts.map +1 -1
- package/dist/components/plugin-frame.js +53 -20
- package/dist/components/plugin-frame.js.map +1 -1
- package/dist/components/plugin-settings.d.ts.map +1 -1
- package/dist/components/plugin-settings.js +151 -26
- package/dist/components/plugin-settings.js.map +1 -1
- package/dist/components/projects.d.ts +0 -8
- package/dist/components/projects.d.ts.map +1 -1
- package/dist/components/projects.js +1 -13
- package/dist/components/projects.js.map +1 -1
- package/dist/components/task-view.d.ts.map +1 -1
- package/dist/components/task-view.js +2 -1
- package/dist/components/task-view.js.map +1 -1
- package/dist/components/ui.d.ts.map +1 -1
- package/dist/components/ui.js +5 -2
- package/dist/components/ui.js.map +1 -1
- package/dist/connect-page.d.ts.map +1 -1
- package/dist/connect-page.js +5 -6
- package/dist/connect-page.js.map +1 -1
- package/dist/console.css +761 -191
- package/dist/console.d.ts +10 -3
- package/dist/console.d.ts.map +1 -1
- package/dist/console.js +33 -13
- package/dist/console.js.map +1 -1
- package/dist/data.d.ts +4 -2
- package/dist/data.d.ts.map +1 -1
- package/dist/data.js +5 -0
- package/dist/data.js.map +1 -1
- package/dist/docs-page.d.ts.map +1 -1
- package/dist/docs-page.js +6 -5
- package/dist/docs-page.js.map +1 -1
- package/dist/home-tiles.d.ts +92 -0
- package/dist/home-tiles.d.ts.map +1 -0
- package/dist/home-tiles.js +294 -0
- package/dist/home-tiles.js.map +1 -0
- package/dist/i18n/en.d.ts +174 -0
- package/dist/i18n/en.d.ts.map +1 -1
- package/dist/i18n/en.js +175 -1
- package/dist/i18n/en.js.map +1 -1
- package/dist/i18n/index.d.ts +4 -2
- package/dist/i18n/index.d.ts.map +1 -1
- package/dist/i18n/index.js +10 -11
- package/dist/i18n/index.js.map +1 -1
- package/dist/i18n/ko.d.ts.map +1 -1
- package/dist/i18n/ko.js +170 -3
- package/dist/i18n/ko.js.map +1 -1
- package/dist/index.d.ts +4 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -3
- package/dist/index.js.map +1 -1
- package/dist/limits.d.ts +6 -0
- package/dist/limits.d.ts.map +1 -1
- package/dist/limits.js +6 -0
- package/dist/limits.js.map +1 -1
- package/dist/plugin/bridge.d.ts +44 -3
- package/dist/plugin/bridge.d.ts.map +1 -1
- package/dist/plugin/bridge.js +26 -0
- package/dist/plugin/bridge.js.map +1 -1
- package/dist/plugin/capabilities.d.ts +7 -1
- package/dist/plugin/capabilities.d.ts.map +1 -1
- package/dist/plugin/capabilities.js +14 -1
- package/dist/plugin/capabilities.js.map +1 -1
- package/dist/plugin/context.d.ts +18 -1
- package/dist/plugin/context.d.ts.map +1 -1
- package/dist/plugin/define.d.ts +1 -0
- package/dist/plugin/define.d.ts.map +1 -1
- package/dist/plugin/define.js +1 -1
- package/dist/plugin/define.js.map +1 -1
- package/dist/plugin/limits.d.ts +5 -2
- package/dist/plugin/limits.d.ts.map +1 -1
- package/dist/plugin/limits.js +5 -2
- package/dist/plugin/limits.js.map +1 -1
- package/dist/plugin/manifest.d.ts +83 -6
- package/dist/plugin/manifest.d.ts.map +1 -1
- package/dist/plugin/manifest.js +111 -7
- package/dist/plugin/manifest.js.map +1 -1
- package/dist/plugin/rest-schemas.d.ts +76 -0
- package/dist/plugin/rest-schemas.d.ts.map +1 -1
- package/dist/plugin/rest-schemas.js +23 -2
- package/dist/plugin/rest-schemas.js.map +1 -1
- package/dist/plugin/sandbox.d.ts +4 -3
- package/dist/plugin/sandbox.d.ts.map +1 -1
- package/dist/plugin/sandbox.js +33 -6
- package/dist/plugin/sandbox.js.map +1 -1
- package/dist/plugin/semver.d.ts +26 -0
- package/dist/plugin/semver.d.ts.map +1 -0
- package/dist/plugin/semver.js +76 -0
- package/dist/plugin/semver.js.map +1 -0
- package/dist/plugin/testing.d.ts.map +1 -1
- package/dist/plugin/testing.js +2 -1
- package/dist/plugin/testing.js.map +1 -1
- package/dist/plugin/web.d.ts +10 -1
- package/dist/plugin/web.d.ts.map +1 -1
- package/dist/plugin/web.js +34 -0
- package/dist/plugin/web.js.map +1 -1
- package/dist/plugin.d.ts +3 -2
- package/dist/plugin.d.ts.map +1 -1
- package/dist/plugin.js +3 -2
- package/dist/plugin.js.map +1 -1
- package/dist/rules.d.ts +2 -2
- package/dist/rules.js +3 -3
- package/dist/schemas.d.ts +497 -2
- package/dist/schemas.d.ts.map +1 -1
- package/dist/schemas.js +96 -9
- package/dist/schemas.js.map +1 -1
- package/dist/use-action.d.ts +8 -2
- package/dist/use-action.d.ts.map +1 -1
- package/dist/use-action.js +13 -6
- package/dist/use-action.js.map +1 -1
- package/dist/vocabulary.d.ts +3 -1
- package/dist/vocabulary.d.ts.map +1 -1
- package/dist/vocabulary.js +3 -0
- package/dist/vocabulary.js.map +1 -1
- package/docs/specs/cli.md +4 -3
- package/docs/specs/plugins.md +46 -33
- package/docs/specs/rest.md +16 -15
- package/package.json +1 -1
- package/skills/building-a-plugin/SKILL.md +8 -7
- package/skills/planning-the-board/references/setting-up.md +2 -1
- package/skills/working-the-board/SKILL.md +3 -3
- package/src/api.ts +6 -0
- package/src/cli/cli.ts +124 -19
- package/src/cli/format.ts +42 -9
- package/src/cli/help.ts +17 -13
- package/src/cli/plugin-check.ts +41 -2
- package/src/cli/plugin-cli.ts +10 -1
- package/src/cli/plugin-init.ts +8 -0
- package/src/client.ts +8 -0
- package/src/compat.ts +1 -1
- package/src/components/board.tsx +3 -3
- package/src/components/decision-form.tsx +75 -19
- package/src/components/decision-list.tsx +45 -13
- package/src/components/event-feed.tsx +7 -1
- package/src/components/goals.tsx +4 -1
- package/src/components/home.tsx +615 -0
- package/src/components/pack-connect.tsx +5 -4
- package/src/components/people.tsx +136 -54
- package/src/components/plugin-frame.tsx +62 -63
- package/src/components/plugin-settings.tsx +428 -87
- package/src/components/projects.tsx +0 -55
- package/src/components/task-view.tsx +7 -0
- package/src/components/ui.tsx +6 -2
- package/src/connect-page.tsx +5 -6
- package/src/console.tsx +80 -30
- package/src/data.ts +9 -2
- package/src/docs-page.tsx +6 -5
- package/src/home-tiles.ts +438 -0
- package/src/i18n/en.ts +178 -0
- package/src/i18n/index.ts +17 -14
- package/src/i18n/ko.ts +174 -3
- package/src/index.ts +7 -5
- package/src/limits.ts +6 -0
- package/src/plugin/bridge.ts +77 -1
- package/src/plugin/capabilities.ts +17 -1
- package/src/plugin/context.ts +18 -1
- package/src/plugin/define.ts +4 -1
- package/src/plugin/limits.ts +5 -2
- package/src/plugin/manifest.ts +154 -10
- package/src/plugin/rest-schemas.ts +27 -1
- package/src/plugin/sandbox.ts +36 -8
- package/src/plugin/semver.ts +89 -0
- package/src/plugin/testing.ts +2 -1
- package/src/plugin/web.ts +49 -0
- package/src/plugin.ts +18 -0
- package/src/rules.ts +3 -3
- package/src/schemas.ts +118 -10
- package/src/styles/console.css +761 -191
- package/src/use-action.ts +16 -7
- package/src/vocabulary.ts +4 -0
- package/template/plugin/console-plugin.json +3 -1
|
@@ -0,0 +1,438 @@
|
|
|
1
|
+
import { MAX_HOME_ROW, MAX_HOME_TILES } from "./limits.js";
|
|
2
|
+
import type { PluginWidgetSizeName } from "./plugin/bridge.js";
|
|
3
|
+
import { PLUGIN_WIDGET_SIZE_CELLS, widgetSizesOf } from "./plugin/manifest.js";
|
|
4
|
+
import type {
|
|
5
|
+
RestHome,
|
|
6
|
+
RestHomeTile,
|
|
7
|
+
RestPlugin,
|
|
8
|
+
RestPluginWidget,
|
|
9
|
+
RestProject,
|
|
10
|
+
} from "./schemas.js";
|
|
11
|
+
|
|
12
|
+
// The front page as a person arranges it (ADR-0061): one grid of square cells, on which the
|
|
13
|
+
// projects and the plugins' widgets are tiles alike, each at a place of its own. What there is
|
|
14
|
+
// comes from what the person may see; where each stands comes from what they kept, one layout
|
|
15
|
+
// for each width of the grid. What is new takes the first room, what they took off stays off,
|
|
16
|
+
// and what they may no longer see waits out of sight for its place. Pure functions, so that the
|
|
17
|
+
// page and the tests read the same arrangement.
|
|
18
|
+
|
|
19
|
+
/** The grid's columns at each width: wide, a laptop's, a tablet's, a phone's. */
|
|
20
|
+
export const HOME_COLUMNS = [12, 8, 6, 4] as const;
|
|
21
|
+
export type HomeColumns = (typeof HOME_COLUMNS)[number];
|
|
22
|
+
|
|
23
|
+
/** The width in pixels from which the grid has so many columns, so that a cell stays of a size to read. */
|
|
24
|
+
const COLUMNS_FROM: readonly (readonly [number, HomeColumns])[] = [
|
|
25
|
+
[1520, 12],
|
|
26
|
+
[900, 8],
|
|
27
|
+
[560, 6],
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
/** The columns of the grid at a width in pixels. */
|
|
31
|
+
export function columnsFor(width: number): HomeColumns {
|
|
32
|
+
return COLUMNS_FROM.find(([from]) => width >= from)?.[1] ?? 4;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export type HomeTileSize = RestHomeTile["size"];
|
|
36
|
+
|
|
37
|
+
/** The cells a tile takes at each size: an icon one, the widgets' family as the phones have it. */
|
|
38
|
+
export const HOME_TILE_CELLS: Readonly<
|
|
39
|
+
Record<HomeTileSize, { readonly columns: number; readonly rows: number }>
|
|
40
|
+
> = { icon: { columns: 1, rows: 1 }, ...PLUGIN_WIDGET_SIZE_CELLS };
|
|
41
|
+
|
|
42
|
+
/** A project on the page: an icon, or a card with what it is. */
|
|
43
|
+
export interface HomeProjectTile {
|
|
44
|
+
readonly kind: "project";
|
|
45
|
+
readonly id: string;
|
|
46
|
+
readonly project: RestProject;
|
|
47
|
+
readonly sizes: readonly HomeTileSize[];
|
|
48
|
+
readonly defaultSize: HomeTileSize;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** A plugin's widget on the page. */
|
|
52
|
+
export interface HomeWidgetTile {
|
|
53
|
+
readonly kind: "widget";
|
|
54
|
+
readonly id: string;
|
|
55
|
+
readonly plugin: RestPlugin;
|
|
56
|
+
readonly widget: RestPluginWidget;
|
|
57
|
+
readonly sizes: readonly PluginWidgetSizeName[];
|
|
58
|
+
readonly defaultSize: PluginWidgetSizeName;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export type HomeTile = HomeProjectTile | HomeWidgetTile;
|
|
62
|
+
|
|
63
|
+
/** A tile where it stands. */
|
|
64
|
+
export interface PlacedTile {
|
|
65
|
+
readonly tile: HomeTile;
|
|
66
|
+
readonly x: number;
|
|
67
|
+
readonly y: number;
|
|
68
|
+
readonly size: HomeTileSize;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const PROJECT_SIZES: readonly HomeTileSize[] = ["icon", "small"];
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Every tile the person may see: the widgets of the plugins that are on, which they hold a role
|
|
75
|
+
* for, and the projects, those a manifest places at the top first and those at the bottom last.
|
|
76
|
+
* An administrator reads every plugin installed, and one that is off has no frame to show.
|
|
77
|
+
*/
|
|
78
|
+
export function homeTiles(
|
|
79
|
+
plugins: readonly RestPlugin[],
|
|
80
|
+
projects: readonly RestProject[],
|
|
81
|
+
): HomeTile[] {
|
|
82
|
+
const widgets = plugins
|
|
83
|
+
.filter((plugin) => plugin.state === "enabled")
|
|
84
|
+
.flatMap((plugin) =>
|
|
85
|
+
plugin.widgets.map((widget): HomeWidgetTile => {
|
|
86
|
+
const { sizes, defaultSize } = widgetSizesOf(widget);
|
|
87
|
+
return {
|
|
88
|
+
kind: "widget",
|
|
89
|
+
id: `widget:${plugin.key}:${widget.key}`,
|
|
90
|
+
plugin,
|
|
91
|
+
widget,
|
|
92
|
+
sizes,
|
|
93
|
+
defaultSize,
|
|
94
|
+
};
|
|
95
|
+
}),
|
|
96
|
+
);
|
|
97
|
+
return [
|
|
98
|
+
...widgets.filter((tile) => tile.widget.place === "home:top"),
|
|
99
|
+
...projects.map(
|
|
100
|
+
(project): HomeProjectTile => ({
|
|
101
|
+
kind: "project",
|
|
102
|
+
id: `project:${project.id}`,
|
|
103
|
+
project,
|
|
104
|
+
sizes: PROJECT_SIZES,
|
|
105
|
+
defaultSize: "icon",
|
|
106
|
+
}),
|
|
107
|
+
),
|
|
108
|
+
...widgets.filter((tile) => tile.widget.place !== "home:top"),
|
|
109
|
+
];
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
interface Footprint {
|
|
113
|
+
readonly w: number;
|
|
114
|
+
readonly h: number;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
interface Place {
|
|
118
|
+
readonly x: number;
|
|
119
|
+
readonly y: number;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* The sizes a tile may have on a grid of so many columns: those that fit it. A widget always has
|
|
124
|
+
* one (ADR-0062); only a console older than the rule can answer one that has none, and then it
|
|
125
|
+
* keeps what it has, drawn as wide as the grid.
|
|
126
|
+
*/
|
|
127
|
+
export function sizesAt(tile: HomeTile, columns: number): readonly HomeTileSize[] {
|
|
128
|
+
const fitting = (tile.sizes as readonly HomeTileSize[]).filter(
|
|
129
|
+
(size) => HOME_TILE_CELLS[size].columns <= columns,
|
|
130
|
+
);
|
|
131
|
+
return fitting.length > 0 ? fitting : tile.sizes;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** The size a tile starts at on a grid of so many columns: its own where that fits, else the largest that does. */
|
|
135
|
+
export function startingSizeAt(tile: HomeTile, columns: number): HomeTileSize {
|
|
136
|
+
const fitting = sizesAt(tile, columns);
|
|
137
|
+
if (fitting.includes(tile.defaultSize)) {
|
|
138
|
+
return tile.defaultSize;
|
|
139
|
+
}
|
|
140
|
+
const area = (size: HomeTileSize) => HOME_TILE_CELLS[size].columns * HOME_TILE_CELLS[size].rows;
|
|
141
|
+
return [...fitting].sort((a, b) => area(b) - area(a))[0] ?? tile.defaultSize;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** The cells a size takes on a grid of so many columns: never wider than the grid. */
|
|
145
|
+
export function footprintOf(size: HomeTileSize, columns: number): Footprint {
|
|
146
|
+
const cells = HOME_TILE_CELLS[size];
|
|
147
|
+
return { w: Math.min(cells.columns, columns), h: cells.rows };
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const inReadingOrder = <T extends Place>(tiles: readonly T[]): T[] =>
|
|
151
|
+
[...tiles].sort((a, b) => a.y - b.y || a.x - b.x);
|
|
152
|
+
|
|
153
|
+
/** The cells taken, each as one number. */
|
|
154
|
+
function occupied(placed: readonly PlacedTile[], columns: number): Set<number> {
|
|
155
|
+
const taken = new Set<number>();
|
|
156
|
+
for (const tile of placed) {
|
|
157
|
+
occupy(taken, tile, footprintOf(tile.size, columns), columns);
|
|
158
|
+
}
|
|
159
|
+
return taken;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function occupy(taken: Set<number>, place: Place, shape: Footprint, columns: number): void {
|
|
163
|
+
for (let row = 0; row < shape.h; row += 1) {
|
|
164
|
+
for (let column = 0; column < shape.w; column += 1) {
|
|
165
|
+
taken.add((place.y + row) * columns + place.x + column);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function fits(
|
|
171
|
+
taken: ReadonlySet<number>,
|
|
172
|
+
place: Place,
|
|
173
|
+
shape: Footprint,
|
|
174
|
+
columns: number,
|
|
175
|
+
): boolean {
|
|
176
|
+
if (place.x < 0 || place.y < 0 || place.x + shape.w > columns) {
|
|
177
|
+
return false;
|
|
178
|
+
}
|
|
179
|
+
for (let row = 0; row < shape.h; row += 1) {
|
|
180
|
+
for (let column = 0; column < shape.w; column += 1) {
|
|
181
|
+
if (taken.has((place.y + row) * columns + place.x + column)) {
|
|
182
|
+
return false;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
return true;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/** The first room for the shape, reading the grid from the top. Below everything there is always some. */
|
|
190
|
+
function firstRoom(taken: ReadonlySet<number>, shape: Footprint, columns: number): Place {
|
|
191
|
+
for (let y = 0; ; y += 1) {
|
|
192
|
+
for (let x = 0; x + shape.w <= columns; x += 1) {
|
|
193
|
+
if (fits(taken, { x, y }, shape, columns)) {
|
|
194
|
+
return { x, y };
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/** The room for the shape nearest a place. */
|
|
201
|
+
function nearestRoom(
|
|
202
|
+
taken: ReadonlySet<number>,
|
|
203
|
+
shape: Footprint,
|
|
204
|
+
near: Place,
|
|
205
|
+
columns: number,
|
|
206
|
+
): Place {
|
|
207
|
+
const bottom = Math.max(0, ...[...taken].map((cell) => Math.floor(cell / columns) + 1));
|
|
208
|
+
let best = firstRoom(taken, shape, columns);
|
|
209
|
+
let distance = (best.x - near.x) ** 2 + (best.y - near.y) ** 2;
|
|
210
|
+
for (let y = 0; y <= Math.max(bottom, near.y); y += 1) {
|
|
211
|
+
for (let x = 0; x + shape.w <= columns; x += 1) {
|
|
212
|
+
const away = (x - near.x) ** 2 + (y - near.y) ** 2;
|
|
213
|
+
if (away < distance && fits(taken, { x, y }, shape, columns)) {
|
|
214
|
+
best = { x, y };
|
|
215
|
+
distance = away;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
return best;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/** What a layout starts from at a width: what was kept for it, or the same order from another. */
|
|
223
|
+
function keptAt(
|
|
224
|
+
home: RestHome | null | undefined,
|
|
225
|
+
columns: HomeColumns,
|
|
226
|
+
): { readonly entries: readonly RestHomeTile[]; readonly placed: boolean } {
|
|
227
|
+
const own = home?.layouts[columns];
|
|
228
|
+
if (own !== undefined) {
|
|
229
|
+
return { entries: own, placed: true };
|
|
230
|
+
}
|
|
231
|
+
for (const other of HOME_COLUMNS) {
|
|
232
|
+
const entries = home?.layouts[other];
|
|
233
|
+
if (entries !== undefined) {
|
|
234
|
+
return { entries: inReadingOrder(entries), placed: false };
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
return { entries: [], placed: false };
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* The page as it stands: what the person kept for this width where they stand, or, kept for
|
|
242
|
+
* another width, the same order packed from the top; a tile they may no longer see stays out, a
|
|
243
|
+
* size it no longer comes in, or that is too wide for this grid, becomes the one it starts at
|
|
244
|
+
* here, and a tile on another's cells moves to the nearest room. What they have not seen yet takes the first room, a plugin's widgets as soon
|
|
245
|
+
* as it is on; what they took off stays off.
|
|
246
|
+
*/
|
|
247
|
+
export function arranged(
|
|
248
|
+
tiles: readonly HomeTile[],
|
|
249
|
+
home: RestHome | null | undefined,
|
|
250
|
+
columns: HomeColumns,
|
|
251
|
+
): PlacedTile[] {
|
|
252
|
+
const byId = new Map(tiles.map((tile) => [tile.id, tile]));
|
|
253
|
+
const hidden = new Set(home?.hidden ?? []);
|
|
254
|
+
const { entries, placed: keepsPlaces } = keptAt(home, columns);
|
|
255
|
+
const taken = new Set<number>();
|
|
256
|
+
const placed: PlacedTile[] = [];
|
|
257
|
+
const seen = new Set<string>();
|
|
258
|
+
const put = (tile: HomeTile, size: HomeTileSize, wanted: Place | undefined) => {
|
|
259
|
+
const shape = footprintOf(size, columns);
|
|
260
|
+
const place =
|
|
261
|
+
wanted === undefined
|
|
262
|
+
? firstRoom(taken, shape, columns)
|
|
263
|
+
: fits(taken, wanted, shape, columns)
|
|
264
|
+
? wanted
|
|
265
|
+
: nearestRoom(taken, shape, wanted, columns);
|
|
266
|
+
occupy(taken, place, shape, columns);
|
|
267
|
+
placed.push({ tile, size, ...place });
|
|
268
|
+
seen.add(tile.id);
|
|
269
|
+
};
|
|
270
|
+
for (const entry of entries) {
|
|
271
|
+
const tile = byId.get(entry.tile);
|
|
272
|
+
if (tile !== undefined && !hidden.has(tile.id) && !seen.has(tile.id)) {
|
|
273
|
+
const size = sizesAt(tile, columns).includes(entry.size)
|
|
274
|
+
? entry.size
|
|
275
|
+
: startingSizeAt(tile, columns);
|
|
276
|
+
put(tile, size, keepsPlaces ? { x: entry.x, y: entry.y } : undefined);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
for (const tile of tiles) {
|
|
280
|
+
if (!hidden.has(tile.id) && !seen.has(tile.id)) {
|
|
281
|
+
put(tile, startingSizeAt(tile, columns), undefined);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
return placed;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/** How many rows the page takes. */
|
|
288
|
+
export function rowsOf(placed: readonly PlacedTile[], columns: number): number {
|
|
289
|
+
return Math.max(0, ...placed.map((tile) => tile.y + footprintOf(tile.size, columns).h));
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
const sameTile = (a: PlacedTile, b: PlacedTile): boolean => a.tile.id === b.tile.id;
|
|
293
|
+
|
|
294
|
+
const overlapping = (a: PlacedTile, b: PlacedTile, columns: number): boolean => {
|
|
295
|
+
const shapeA = footprintOf(a.size, columns);
|
|
296
|
+
const shapeB = footprintOf(b.size, columns);
|
|
297
|
+
return (
|
|
298
|
+
a.x < b.x + shapeB.w && b.x < a.x + shapeA.w && a.y < b.y + shapeB.h && b.y < a.y + shapeA.h
|
|
299
|
+
);
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
/** The tile where it is to stand, and those on its cells moved to the nearest room. */
|
|
303
|
+
function makingRoom(
|
|
304
|
+
others: readonly PlacedTile[],
|
|
305
|
+
at: PlacedTile,
|
|
306
|
+
columns: number,
|
|
307
|
+
): readonly PlacedTile[] {
|
|
308
|
+
const inTheWay = others.filter((tile) => overlapping(tile, at, columns));
|
|
309
|
+
const staying = [...others.filter((tile) => !overlapping(tile, at, columns)), at];
|
|
310
|
+
const taken = occupied(staying, columns);
|
|
311
|
+
const moved = inReadingOrder(inTheWay).map((tile) => {
|
|
312
|
+
const shape = footprintOf(tile.size, columns);
|
|
313
|
+
const place = nearestRoom(taken, shape, tile, columns);
|
|
314
|
+
occupy(taken, place, shape, columns);
|
|
315
|
+
return { ...tile, ...place };
|
|
316
|
+
});
|
|
317
|
+
return [...staying, ...moved];
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* The page with a tile moved to a place, as a phone's home screen does it. Where the place is
|
|
322
|
+
* free it stands there. On a tile of its own shape, the tiles of that shape between the two
|
|
323
|
+
* places each move one place towards the one it left, so that a row of icons closes up. A smaller
|
|
324
|
+
* tile gives way to the nearest room, and a larger one stays: the tile lands in the room beside it.
|
|
325
|
+
*/
|
|
326
|
+
export function moved(
|
|
327
|
+
placed: readonly PlacedTile[],
|
|
328
|
+
id: string,
|
|
329
|
+
x: number,
|
|
330
|
+
y: number,
|
|
331
|
+
columns: number,
|
|
332
|
+
): readonly PlacedTile[] {
|
|
333
|
+
const moving = placed.find((tile) => tile.tile.id === id);
|
|
334
|
+
if (moving === undefined) {
|
|
335
|
+
return placed;
|
|
336
|
+
}
|
|
337
|
+
const shape = footprintOf(moving.size, columns);
|
|
338
|
+
const at: PlacedTile = {
|
|
339
|
+
...moving,
|
|
340
|
+
x: Math.max(0, Math.min(x, columns - shape.w)),
|
|
341
|
+
y: Math.max(0, Math.min(y, MAX_HOME_ROW)),
|
|
342
|
+
};
|
|
343
|
+
const others = placed.filter((tile) => !sameTile(tile, moving));
|
|
344
|
+
const inTheWay = others.filter((tile) => overlapping(tile, at, columns));
|
|
345
|
+
const [first] = inTheWay;
|
|
346
|
+
if (first === undefined) {
|
|
347
|
+
return [...others, at];
|
|
348
|
+
}
|
|
349
|
+
const shapeOf = (tile: PlacedTile) => footprintOf(tile.size, columns);
|
|
350
|
+
const sameShape = (tile: PlacedTile) =>
|
|
351
|
+
shapeOf(tile).w === shape.w && shapeOf(tile).h === shape.h;
|
|
352
|
+
if (inTheWay.length === 1 && first.x === at.x && first.y === at.y && sameShape(first)) {
|
|
353
|
+
const peers = inReadingOrder(placed.filter(sameShape));
|
|
354
|
+
const places = peers.map(({ x: px, y: py }) => ({ x: px, y: py }));
|
|
355
|
+
const order = peers.filter((tile) => !sameTile(tile, moving));
|
|
356
|
+
order.splice(
|
|
357
|
+
peers.findIndex((tile) => sameTile(tile, first)),
|
|
358
|
+
0,
|
|
359
|
+
moving,
|
|
360
|
+
);
|
|
361
|
+
return [
|
|
362
|
+
...placed.filter((tile) => !sameShape(tile)),
|
|
363
|
+
...order.map((tile, index) => ({ ...tile, ...places[index] })),
|
|
364
|
+
];
|
|
365
|
+
}
|
|
366
|
+
const area = (tile: PlacedTile) => shapeOf(tile).w * shapeOf(tile).h;
|
|
367
|
+
if (inTheWay.some((tile) => area(tile) > area(moving))) {
|
|
368
|
+
return [...others, { ...at, ...nearestRoom(occupied(others, columns), shape, at, columns) }];
|
|
369
|
+
}
|
|
370
|
+
return makingRoom(others, at, columns);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/** How far apart in time the tiles in the way move, by the cells between them, in milliseconds. */
|
|
374
|
+
const WAIT_PER_CELL_MS = 22;
|
|
375
|
+
const LONGEST_WAIT_MS = 130;
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* How long a tile waits before it gives way to the tile being put down at `from`: the further off
|
|
379
|
+
* the later, so that the push runs outward from there, as water does.
|
|
380
|
+
*/
|
|
381
|
+
export function waitToGiveWay(tile: Place, from: Place): number {
|
|
382
|
+
const cells = Math.hypot(tile.x - from.x, tile.y - from.y);
|
|
383
|
+
return Math.min(Math.round(cells * WAIT_PER_CELL_MS), LONGEST_WAIT_MS);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/** The page with a tile at another size, where it stands; what its new cells cover moves aside. */
|
|
387
|
+
export function resized(
|
|
388
|
+
placed: readonly PlacedTile[],
|
|
389
|
+
id: string,
|
|
390
|
+
size: HomeTileSize,
|
|
391
|
+
columns: number,
|
|
392
|
+
): readonly PlacedTile[] {
|
|
393
|
+
const tile = placed.find((candidate) => candidate.tile.id === id);
|
|
394
|
+
if (tile === undefined) {
|
|
395
|
+
return placed;
|
|
396
|
+
}
|
|
397
|
+
const at = { ...tile, size, x: Math.min(tile.x, columns - footprintOf(size, columns).w) };
|
|
398
|
+
return makingRoom(
|
|
399
|
+
placed.filter((candidate) => !sameTile(candidate, tile)),
|
|
400
|
+
at,
|
|
401
|
+
columns,
|
|
402
|
+
);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/** The page with a tile back on it, at the first room, at the size it starts at. */
|
|
406
|
+
export function added(
|
|
407
|
+
placed: readonly PlacedTile[],
|
|
408
|
+
tile: HomeTile,
|
|
409
|
+
columns: number,
|
|
410
|
+
): readonly PlacedTile[] {
|
|
411
|
+
const size = startingSizeAt(tile, columns);
|
|
412
|
+
const room = firstRoom(occupied(placed, columns), footprintOf(size, columns), columns);
|
|
413
|
+
return [...placed, { tile, size, ...room }];
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
/** What is kept of the page: the layout at this width, the others as they were. */
|
|
417
|
+
export function homeWith(
|
|
418
|
+
home: RestHome | null | undefined,
|
|
419
|
+
placed: readonly PlacedTile[],
|
|
420
|
+
hidden: readonly string[],
|
|
421
|
+
columns: HomeColumns,
|
|
422
|
+
): RestHome {
|
|
423
|
+
const shown = new Set(placed.map((tile) => tile.tile.id));
|
|
424
|
+
// What the person may not see for now keeps its place, for the day it is back.
|
|
425
|
+
const away = (home?.layouts[columns] ?? []).filter(
|
|
426
|
+
(entry) => !shown.has(entry.tile) && !hidden.includes(entry.tile),
|
|
427
|
+
);
|
|
428
|
+
const entries: RestHomeTile[] = inReadingOrder(placed).map(({ tile, x, y, size }) => ({
|
|
429
|
+
tile: tile.id,
|
|
430
|
+
x,
|
|
431
|
+
y,
|
|
432
|
+
size,
|
|
433
|
+
}));
|
|
434
|
+
return {
|
|
435
|
+
hidden: hidden.slice(0, MAX_HOME_TILES),
|
|
436
|
+
layouts: { ...home?.layouts, [columns]: [...entries, ...away].slice(0, MAX_HOME_TILES) },
|
|
437
|
+
};
|
|
438
|
+
}
|