@motion-proto/live-tokens 0.80.0 → 0.81.0
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/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.81.0 — Skill Atlas links name a block
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- **A Skill Atlas link opens one block.** Clicking a card or badge writes its
|
|
8
|
+
link to the address bar, named after the card title and the badge label:
|
|
9
|
+
`#set-type/write-the-font-pairing/voice`. Opening the link selects that
|
|
10
|
+
block and scrolls both panes to it. `#set-type` still opens the skill, and
|
|
11
|
+
a link to a block that no longer exists opens its skill.
|
|
12
|
+
|
|
3
13
|
## 0.80.0 — The build runs the design checks
|
|
4
14
|
|
|
5
15
|
### Added
|
package/package.json
CHANGED
|
@@ -1,19 +1,20 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import { tick } from 'svelte';
|
|
2
|
+
import { onMount, tick } from 'svelte';
|
|
3
3
|
import { navigate } from '../core/routing/router';
|
|
4
4
|
import Button from '../../system/components/Button.svelte';
|
|
5
5
|
import TabBar from '../../system/components/TabBar.svelte';
|
|
6
6
|
import SourcePane from './SourcePane.svelte';
|
|
7
7
|
import TreeCanvas from './TreeCanvas.svelte';
|
|
8
|
+
import { linkHash, linkTargets, resolveLink } from './atlasLink';
|
|
8
9
|
import { SKILL_DOC, skillDocs } from './skillSources';
|
|
9
10
|
import { skillTrees } from './skillTrees';
|
|
10
11
|
import type { LineRange, Selection } from './types';
|
|
11
12
|
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
const linked = window.location.hash
|
|
15
|
-
let active = $state(linked
|
|
16
|
-
let selection: Selection | null = $state(null);
|
|
13
|
+
// `#set-type` opens that skill and `#set-type/write-the-font-pairing/voice`
|
|
14
|
+
// also selects that block, so a link can hand someone one step of one tree.
|
|
15
|
+
const linked = resolveLink(window.location.hash, skillTrees);
|
|
16
|
+
let active = $state(linked?.skill ?? Object.keys(skillTrees)[0]);
|
|
17
|
+
let selection: Selection | null = $state(linked?.target ?? null);
|
|
17
18
|
/** Which of the skill's documents the source pane shows. */
|
|
18
19
|
let doc: string = $state(SKILL_DOC);
|
|
19
20
|
|
|
@@ -43,27 +44,21 @@
|
|
|
43
44
|
|
|
44
45
|
function selectTarget(key: string, label: string, range: LineRange) {
|
|
45
46
|
selection = { key, label, lines: range };
|
|
47
|
+
shareSelection();
|
|
46
48
|
// Every range cites SKILL.md, so a step selected while a reference is open
|
|
47
49
|
// would otherwise highlight nothing.
|
|
48
50
|
doc = SKILL_DOC;
|
|
49
51
|
scrollSourceTo(range[0]);
|
|
50
52
|
}
|
|
51
53
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
...(node.lines ? [{ key: node.id, label: node.title, lines: node.lines }] : []),
|
|
56
|
-
...(node.chips ?? []).map((chip, i) => ({
|
|
57
|
-
key: `${node.id}:${i}`,
|
|
58
|
-
label: chip.label,
|
|
59
|
-
lines: chip.lines,
|
|
60
|
-
})),
|
|
61
|
-
]);
|
|
54
|
+
function shareSelection() {
|
|
55
|
+
const target = linkTargets(tree).find((t) => t.key === selection?.key) ?? null;
|
|
56
|
+
history.replaceState(null, '', `${window.location.pathname}${linkHash(active, target)}`);
|
|
62
57
|
}
|
|
63
58
|
|
|
64
59
|
/** Reverse lookup: the narrowest target whose range covers this line wins. */
|
|
65
60
|
function selectFromLine(lineNumber: number) {
|
|
66
|
-
const covering =
|
|
61
|
+
const covering = linkTargets(tree)
|
|
67
62
|
.filter(({ lines: [from, to] }) => lineNumber >= from && lineNumber <= to)
|
|
68
63
|
.sort((a, b) => a.lines[1] - a.lines[0] - (b.lines[1] - b.lines[0]));
|
|
69
64
|
|
|
@@ -71,8 +66,13 @@
|
|
|
71
66
|
if (!best) return;
|
|
72
67
|
|
|
73
68
|
selection = best;
|
|
69
|
+
shareSelection();
|
|
70
|
+
scrollTreeTo(best.key);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function scrollTreeTo(key: string) {
|
|
74
74
|
treePane
|
|
75
|
-
?.querySelector(`[data-node="${
|
|
75
|
+
?.querySelector(`[data-node="${key.split(':')[0]}"]`)
|
|
76
76
|
?.scrollIntoView({ block: 'center', behavior: 'smooth' });
|
|
77
77
|
}
|
|
78
78
|
|
|
@@ -88,11 +88,34 @@
|
|
|
88
88
|
active = id;
|
|
89
89
|
selection = null;
|
|
90
90
|
doc = SKILL_DOC;
|
|
91
|
-
|
|
91
|
+
shareSelection();
|
|
92
92
|
treePane?.scrollTo({ top: 0 });
|
|
93
93
|
sourcePane?.scrollTo({ top: 0 });
|
|
94
94
|
}
|
|
95
95
|
|
|
96
|
+
function openLink(hash: string) {
|
|
97
|
+
const link = resolveLink(hash, skillTrees);
|
|
98
|
+
if (!link) return;
|
|
99
|
+
active = link.skill;
|
|
100
|
+
selection = link.target;
|
|
101
|
+
doc = SKILL_DOC;
|
|
102
|
+
if (!link.target) return;
|
|
103
|
+
const { key, lines: [from] } = link.target;
|
|
104
|
+
// The cards and source rows of a newly opened tab exist only after render.
|
|
105
|
+
tick().then(() => {
|
|
106
|
+
scrollTreeTo(key);
|
|
107
|
+
scrollSourceTo(from);
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
onMount(() => {
|
|
112
|
+
openLink(window.location.hash);
|
|
113
|
+
// A link pasted into this tab changes only the hash, so the page never reloads.
|
|
114
|
+
const onHash = () => openLink(window.location.hash);
|
|
115
|
+
window.addEventListener('hashchange', onHash);
|
|
116
|
+
return () => window.removeEventListener('hashchange', onHash);
|
|
117
|
+
});
|
|
118
|
+
|
|
96
119
|
let treePane: HTMLElement | undefined = $state();
|
|
97
120
|
let sourcePane: HTMLElement | undefined = $state();
|
|
98
121
|
</script>
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { Selection, SkillTree } from './types';
|
|
2
|
+
|
|
3
|
+
/** A selectable target with the path segment its shared link names it by. */
|
|
4
|
+
export interface LinkedTarget extends Selection {
|
|
5
|
+
path: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function slug(text: string): string {
|
|
9
|
+
return text
|
|
10
|
+
.toLowerCase()
|
|
11
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
12
|
+
.replace(/^-|-$/g, '');
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Every target in the tree, node and chip alike. A chip's path nests under its card's. */
|
|
16
|
+
export function linkTargets(tree: SkillTree): LinkedTarget[] {
|
|
17
|
+
return tree.nodes.flatMap((node) => [
|
|
18
|
+
...(node.lines
|
|
19
|
+
? [{ key: node.id, label: node.title, lines: node.lines, path: slug(node.title) }]
|
|
20
|
+
: []),
|
|
21
|
+
...(node.chips ?? []).map((chip, i) => ({
|
|
22
|
+
key: `${node.id}:${i}`,
|
|
23
|
+
label: chip.label,
|
|
24
|
+
lines: chip.lines,
|
|
25
|
+
path: `${slug(node.title)}/${slug(chip.label)}`,
|
|
26
|
+
})),
|
|
27
|
+
]);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function linkHash(skill: string, target: LinkedTarget | null): string {
|
|
31
|
+
return target ? `#${skill}/${target.path}` : `#${skill}`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Reads `#set-type/write-the-font-pairing/voice`. An unknown block still opens its skill. */
|
|
35
|
+
export function resolveLink(
|
|
36
|
+
hash: string,
|
|
37
|
+
trees: Record<string, SkillTree>,
|
|
38
|
+
): { skill: string; target: LinkedTarget | null } | null {
|
|
39
|
+
const [skill, ...rest] = decodeURIComponent(hash.replace(/^#/, '')).split('/');
|
|
40
|
+
if (!(skill in trees)) return null;
|
|
41
|
+
const path = rest.join('/');
|
|
42
|
+
return { skill, target: linkTargets(trees[skill]).find((t) => t.path === path) ?? null };
|
|
43
|
+
}
|