@tanishqkancharla/diffmap 0.3.0 → 0.4.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/README.md +97 -1
- package/index.html +6 -0
- package/package.json +3 -2
- package/src/changes.ts +15 -0
- package/src/codeNotes.ts +123 -0
- package/src/errors.ts +5 -0
- package/src/git.test.ts +13 -1
- package/src/git.ts +15 -2
- package/src/host/HostHome.tsx +22 -6
- package/src/host/home.md +148 -102
- package/src/host/home.test.ts +6 -7
- package/src/parseFence.ts +57 -17
- package/src/parseViewer.test.ts +216 -2
- package/src/parseViewer.ts +85 -19
- package/src/services.ts +289 -0
- package/src/share.ts +1 -1
- package/src/viewer/CallStackDiff.tsx +90 -24
- package/src/viewer/ChangesCard.tsx +133 -43
- package/src/viewer/CodeBlock.tsx +131 -0
- package/src/viewer/Fence.tsx +22 -2
- package/src/viewer/ServiceDiagramBlock.tsx +323 -0
- package/src/viewer/ServiceStage.tsx +988 -0
- package/src/viewer/SourceDiffPanel.tsx +11 -8
- package/src/viewer/TableOfContents.tsx +3 -0
- package/src/viewer/ViewerApp.tsx +18 -3
- package/src/viewer/notes.tsx +126 -0
- package/src/viewer/serviceLayout.ts +189 -0
- package/vite.config.ts +1 -0
- package/vite.gist.config.ts +1 -0
package/src/services.ts
ADDED
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
import { parseAnnotation, type SourceAnnotation } from "./annotations.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A `services` fence: services that hold state, nest sub-services, and send
|
|
5
|
+
* each other events. Steps replay the events in order. Every event in a step
|
|
6
|
+
* is in flight at once; the step's state updates land when they arrive.
|
|
7
|
+
*/
|
|
8
|
+
export type ServiceDiagram = {
|
|
9
|
+
services: Service[];
|
|
10
|
+
steps: ServiceStep[];
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export type Service = {
|
|
14
|
+
name: string;
|
|
15
|
+
label: string | undefined;
|
|
16
|
+
description: string | undefined;
|
|
17
|
+
state: ServiceState[];
|
|
18
|
+
children: Service[];
|
|
19
|
+
annotation: SourceAnnotation | undefined;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export type ServiceState = {
|
|
23
|
+
key: string;
|
|
24
|
+
value: string;
|
|
25
|
+
description: string | undefined;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export type ServiceStep = {
|
|
29
|
+
/** What happens in this step, in a sentence. */
|
|
30
|
+
caption: string;
|
|
31
|
+
description: string | undefined;
|
|
32
|
+
events: ServiceEvent[];
|
|
33
|
+
updates: ServiceUpdate[];
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export type ServiceEvent = {
|
|
37
|
+
from: string;
|
|
38
|
+
to: string;
|
|
39
|
+
label: string;
|
|
40
|
+
/** Shown beside the event's label while its step is current. */
|
|
41
|
+
note: string | undefined;
|
|
42
|
+
annotation: SourceAnnotation | undefined;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export type ServiceUpdate = {
|
|
46
|
+
service: string;
|
|
47
|
+
key: string;
|
|
48
|
+
value: string;
|
|
49
|
+
/** Shown beside the changed state while its step is current. */
|
|
50
|
+
note: string | undefined;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
export type ServicesParse =
|
|
54
|
+
| { ok: true; diagram: ServiceDiagram }
|
|
55
|
+
| { ok: false; line: number; reason: string };
|
|
56
|
+
|
|
57
|
+
const name = String.raw`[A-Za-z_]\w*`;
|
|
58
|
+
const serviceLine = new RegExp(
|
|
59
|
+
String.raw`^service\s+(${name})(?:\s+"([^"]*)")?\s*$`,
|
|
60
|
+
);
|
|
61
|
+
const stateLine = new RegExp(String.raw`^state\s+(${name})\s*=\s*(.+)$`);
|
|
62
|
+
const stepLine = /^step\b(?:\s+"([^"]*)")?\s*$/;
|
|
63
|
+
const eventLine = new RegExp(
|
|
64
|
+
String.raw`^(${name})\s*->\s*(${name})(?:\s*:\s*(.*))?$`,
|
|
65
|
+
);
|
|
66
|
+
const updateLine = new RegExp(String.raw`^(${name})\.(${name})\s*=\s*(.+)$`);
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* ```services
|
|
70
|
+
* service API "HTTP server" [[src/api.ts]]
|
|
71
|
+
* state requests = 0
|
|
72
|
+
* service Auth
|
|
73
|
+
* state attempts = 0
|
|
74
|
+
*
|
|
75
|
+
* step "Check the password"
|
|
76
|
+
* API -> Auth: verify(alice) [[src/api.ts#login]]
|
|
77
|
+
* Auth.attempts = 1
|
|
78
|
+
* ```
|
|
79
|
+
*/
|
|
80
|
+
export function parseServiceDiagram(source: string): ServicesParse {
|
|
81
|
+
const services: Service[] = [];
|
|
82
|
+
const byName = new Map<string, Service>();
|
|
83
|
+
const steps: ServiceStep[] = [];
|
|
84
|
+
// Open services, innermost last, with the indent of their `service` line.
|
|
85
|
+
const open: { indent: number; service: Service }[] = [];
|
|
86
|
+
// Where a `>` line's text goes: a description on a service, state, or
|
|
87
|
+
// step, or a note on an event or update.
|
|
88
|
+
let described: { indent: number; append: (text: string) => void } | undefined;
|
|
89
|
+
const describe = (indent: number, item: { description?: string }) => {
|
|
90
|
+
described = {
|
|
91
|
+
indent,
|
|
92
|
+
append: (more) => {
|
|
93
|
+
item.description =
|
|
94
|
+
item.description === undefined ? more : `${item.description} ${more}`;
|
|
95
|
+
},
|
|
96
|
+
};
|
|
97
|
+
};
|
|
98
|
+
const annotate = (indent: number, item: { note?: string }) => {
|
|
99
|
+
described = {
|
|
100
|
+
indent,
|
|
101
|
+
append: (more) => {
|
|
102
|
+
item.note = item.note === undefined ? more : `${item.note} ${more}`;
|
|
103
|
+
},
|
|
104
|
+
};
|
|
105
|
+
};
|
|
106
|
+
const lines = source.split("\n");
|
|
107
|
+
for (const [index, raw] of lines.entries()) {
|
|
108
|
+
const line = index + 1;
|
|
109
|
+
const fail = (reason: string): ServicesParse => ({
|
|
110
|
+
ok: false,
|
|
111
|
+
line,
|
|
112
|
+
reason,
|
|
113
|
+
});
|
|
114
|
+
const text = raw.trim();
|
|
115
|
+
if (text.length === 0 || text.startsWith("%%")) continue;
|
|
116
|
+
const indent = raw.replace(/\t/g, " ").search(/\S/);
|
|
117
|
+
if (text.startsWith(">")) {
|
|
118
|
+
if (described === undefined || indent <= described.indent)
|
|
119
|
+
return fail(
|
|
120
|
+
"Put a > line directly below the service, state, step, event, or update it describes, indented further.",
|
|
121
|
+
);
|
|
122
|
+
described.append(text.slice(1).trim());
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
const { text: body, references } = parseAnnotation(text);
|
|
126
|
+
const annotation =
|
|
127
|
+
references.length === 0 ? undefined : { text: body.trim(), references };
|
|
128
|
+
const step = steps.at(-1);
|
|
129
|
+
|
|
130
|
+
const serviceMatch = serviceLine.exec(body.trim());
|
|
131
|
+
if (serviceMatch !== null) {
|
|
132
|
+
if (step !== undefined)
|
|
133
|
+
return fail("Declare every service before the first step.");
|
|
134
|
+
const serviceName = serviceMatch[1]!;
|
|
135
|
+
if (byName.has(serviceName))
|
|
136
|
+
return fail(`Service ${serviceName} is declared twice.`);
|
|
137
|
+
while (open.length > 0 && open.at(-1)!.indent >= indent) open.pop();
|
|
138
|
+
const service: Service = {
|
|
139
|
+
name: serviceName,
|
|
140
|
+
label: serviceMatch[2],
|
|
141
|
+
description: undefined,
|
|
142
|
+
state: [],
|
|
143
|
+
children: [],
|
|
144
|
+
annotation:
|
|
145
|
+
annotation === undefined
|
|
146
|
+
? undefined
|
|
147
|
+
: { ...annotation, text: serviceName },
|
|
148
|
+
};
|
|
149
|
+
(open.at(-1)?.service.children ?? services).push(service);
|
|
150
|
+
byName.set(serviceName, service);
|
|
151
|
+
open.push({ indent, service });
|
|
152
|
+
describe(indent, service);
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const stateMatch = stateLine.exec(text);
|
|
157
|
+
if (stateMatch !== null) {
|
|
158
|
+
if (step !== undefined)
|
|
159
|
+
return fail(
|
|
160
|
+
"Declare state under its service, before the first step. Change it in a step with Service.key = value.",
|
|
161
|
+
);
|
|
162
|
+
while (open.length > 0 && open.at(-1)!.indent >= indent) open.pop();
|
|
163
|
+
const owner = open.at(-1)?.service;
|
|
164
|
+
if (owner === undefined)
|
|
165
|
+
return fail("Indent state under the service that owns it.");
|
|
166
|
+
const key = stateMatch[1]!;
|
|
167
|
+
if (owner.state.some((entry) => entry.key === key))
|
|
168
|
+
return fail(`${owner.name}.${key} is declared twice.`);
|
|
169
|
+
const entry = {
|
|
170
|
+
key,
|
|
171
|
+
value: stateMatch[2]!.trim(),
|
|
172
|
+
description: undefined,
|
|
173
|
+
};
|
|
174
|
+
owner.state.push(entry);
|
|
175
|
+
describe(indent, entry);
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const stepMatch = stepLine.exec(text);
|
|
180
|
+
if (stepMatch !== null) {
|
|
181
|
+
if (indent !== 0) return fail("Start step lines at column 0.");
|
|
182
|
+
const caption = stepMatch[1]?.trim();
|
|
183
|
+
if (caption === undefined || caption === "")
|
|
184
|
+
return fail(
|
|
185
|
+
'Give the step a caption that says what happens, like step "Auth checks the password".',
|
|
186
|
+
);
|
|
187
|
+
const next: ServiceStep = {
|
|
188
|
+
caption,
|
|
189
|
+
description: undefined,
|
|
190
|
+
events: [],
|
|
191
|
+
updates: [],
|
|
192
|
+
};
|
|
193
|
+
steps.push(next);
|
|
194
|
+
describe(indent, next);
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (step !== undefined && indent > 0) {
|
|
199
|
+
const event = eventLine.exec(body.trim());
|
|
200
|
+
if (event !== null) {
|
|
201
|
+
for (const service of [event[1]!, event[2]!])
|
|
202
|
+
if (!byName.has(service)) return fail(`Unknown service ${service}.`);
|
|
203
|
+
const label = (event[3] ?? "").trim();
|
|
204
|
+
const entry: ServiceEvent = {
|
|
205
|
+
from: event[1]!,
|
|
206
|
+
to: event[2]!,
|
|
207
|
+
label,
|
|
208
|
+
note: undefined,
|
|
209
|
+
annotation:
|
|
210
|
+
annotation === undefined
|
|
211
|
+
? undefined
|
|
212
|
+
: {
|
|
213
|
+
...annotation,
|
|
214
|
+
text: `${event[1]} → ${event[2]}${label === "" ? "" : `: ${label}`}`,
|
|
215
|
+
},
|
|
216
|
+
};
|
|
217
|
+
step.events.push(entry);
|
|
218
|
+
annotate(indent, entry);
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
const update = updateLine.exec(text);
|
|
222
|
+
if (update !== null) {
|
|
223
|
+
const owner = byName.get(update[1]!);
|
|
224
|
+
if (owner === undefined) return fail(`Unknown service ${update[1]}.`);
|
|
225
|
+
if (!owner.state.some((entry) => entry.key === update[2]))
|
|
226
|
+
return fail(
|
|
227
|
+
`${owner.name} has no state ${update[2]}. Declare it with state ${update[2]} = <initial value>.`,
|
|
228
|
+
);
|
|
229
|
+
const entry: ServiceUpdate = {
|
|
230
|
+
service: owner.name,
|
|
231
|
+
key: update[2]!,
|
|
232
|
+
value: update[3]!.trim(),
|
|
233
|
+
note: undefined,
|
|
234
|
+
};
|
|
235
|
+
step.updates.push(entry);
|
|
236
|
+
annotate(indent, entry);
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
239
|
+
return fail(
|
|
240
|
+
`Expected an event (From -> To: label) or a state update (Service.key = value), got "${text}".`,
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
return fail(
|
|
245
|
+
`Expected service <Name>, state <key> = <value>, or step "<caption>", got "${text}".`,
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
if (services.length === 0)
|
|
249
|
+
return { ok: false, line: 1, reason: "Declare at least one service." };
|
|
250
|
+
return { ok: true, diagram: { services, steps } };
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/** Every service in declaration order, with its parent. */
|
|
254
|
+
export function flattenServices(
|
|
255
|
+
services: Service[],
|
|
256
|
+
parent?: Service,
|
|
257
|
+
): { service: Service; parent: Service | undefined }[] {
|
|
258
|
+
return services.flatMap((service) => [
|
|
259
|
+
{ service, parent },
|
|
260
|
+
...flattenServices(service.children, service),
|
|
261
|
+
]);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/** State after the first `step` steps; step 0 is the declared state. */
|
|
265
|
+
export function serviceStateAt(diagram: ServiceDiagram, step: number) {
|
|
266
|
+
const state = new Map(
|
|
267
|
+
flattenServices(diagram.services).map(({ service }) => [
|
|
268
|
+
service.name,
|
|
269
|
+
new Map(service.state.map((entry) => [entry.key, entry.value])),
|
|
270
|
+
]),
|
|
271
|
+
);
|
|
272
|
+
for (const { updates } of diagram.steps.slice(0, step))
|
|
273
|
+
for (const update of updates)
|
|
274
|
+
state.get(update.service)!.set(update.key, update.value);
|
|
275
|
+
return state;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
export function serviceAnnotations(diagram: ServiceDiagram) {
|
|
279
|
+
return [
|
|
280
|
+
...flattenServices(diagram.services).flatMap(({ service }) =>
|
|
281
|
+
service.annotation === undefined ? [] : [service.annotation],
|
|
282
|
+
),
|
|
283
|
+
...diagram.steps.flatMap((step) =>
|
|
284
|
+
step.events.flatMap((event) =>
|
|
285
|
+
event.annotation === undefined ? [] : [event.annotation],
|
|
286
|
+
),
|
|
287
|
+
),
|
|
288
|
+
];
|
|
289
|
+
}
|
package/src/share.ts
CHANGED
|
@@ -45,7 +45,7 @@ export async function shareMarkdownFile(input: {
|
|
|
45
45
|
const changes = await readDocumentChanges(input.workspaceRoot, parsed);
|
|
46
46
|
if (changes instanceof Error) return changes;
|
|
47
47
|
// An uncommitted changes card shows the tree as it is when you share.
|
|
48
|
-
for (const rev of parsed.
|
|
48
|
+
for (const { rev } of parsed.changeCards) {
|
|
49
49
|
if (changes.has(rev)) continue;
|
|
50
50
|
const loaded = await readChanges(input.workspaceRoot, rev);
|
|
51
51
|
if (loaded instanceof Error) return loaded;
|
|
@@ -5,17 +5,24 @@ import {
|
|
|
5
5
|
LARGE_SCALE,
|
|
6
6
|
spacing,
|
|
7
7
|
text,
|
|
8
|
+
Tooltip,
|
|
8
9
|
} from "maui";
|
|
9
10
|
import { defineVars, style, useStyles } from "purse-styles";
|
|
10
11
|
import { codeFontFamily } from "../codeFont.js";
|
|
11
12
|
import type { SourceAnnotation, SourceNavigation } from "../annotations.js";
|
|
13
|
+
import { lineSymbolMatches, type CodeNote } from "../codeNotes.js";
|
|
14
|
+
import { NoteText, NoteTokens } from "./notes.tsx";
|
|
12
15
|
import { pierreShell } from "./pierre.js";
|
|
13
16
|
|
|
14
17
|
export function CallStackDiff(
|
|
15
|
-
props: {
|
|
18
|
+
props: {
|
|
19
|
+
lines: SourceAnnotation[];
|
|
20
|
+
highlights: number[];
|
|
21
|
+
notes: CodeNote[];
|
|
22
|
+
} & SourceNavigation,
|
|
16
23
|
) {
|
|
17
24
|
const shell = useStyles(pierreShell, styles.shell);
|
|
18
|
-
const
|
|
25
|
+
const rowClassName = useStyles(styles.row);
|
|
19
26
|
return (
|
|
20
27
|
<div className={shell} data-diffmap-kind="callstack">
|
|
21
28
|
{props.lines.map((line, index) => {
|
|
@@ -28,6 +35,32 @@ export function CallStackDiff(
|
|
|
28
35
|
const tree = /^[ \t│┃├└─]*/u.exec(label)![0];
|
|
29
36
|
const description = label.slice(tree.length);
|
|
30
37
|
const comment = description.indexOf("#");
|
|
38
|
+
const linked = line.references.length > 0;
|
|
39
|
+
const row = index + 1;
|
|
40
|
+
const lineNote = props.notes.find(
|
|
41
|
+
(note) =>
|
|
42
|
+
note.target.kind === "lines" &&
|
|
43
|
+
note.target.start <= row &&
|
|
44
|
+
row <= note.target.end,
|
|
45
|
+
);
|
|
46
|
+
const matches = lineSymbolMatches(description, props.notes);
|
|
47
|
+
const rowProps = {
|
|
48
|
+
className: rowClassName,
|
|
49
|
+
"data-change": sign,
|
|
50
|
+
"data-highlighted": props.highlights.includes(row) || undefined,
|
|
51
|
+
"data-note":
|
|
52
|
+
lineNote === undefined
|
|
53
|
+
? undefined
|
|
54
|
+
: lineNote.target.kind === "lines" &&
|
|
55
|
+
lineNote.target.start === row
|
|
56
|
+
? "start"
|
|
57
|
+
: "",
|
|
58
|
+
// A native title would show beside the note tooltip.
|
|
59
|
+
title:
|
|
60
|
+
lineNote === undefined && matches.length === 0
|
|
61
|
+
? description
|
|
62
|
+
: undefined,
|
|
63
|
+
};
|
|
31
64
|
const content = (
|
|
32
65
|
<>
|
|
33
66
|
<span className="diff-sign" aria-hidden="true">
|
|
@@ -40,12 +73,21 @@ export function CallStackDiff(
|
|
|
40
73
|
))}
|
|
41
74
|
</span>
|
|
42
75
|
<span className="stack-label">
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
76
|
+
<NoteTokens
|
|
77
|
+
tokens={
|
|
78
|
+
comment === -1
|
|
79
|
+
? [{ content: description }]
|
|
80
|
+
: [
|
|
81
|
+
{ content: description.slice(0, comment) },
|
|
82
|
+
{
|
|
83
|
+
content: description.slice(comment),
|
|
84
|
+
className: "stack-comment",
|
|
85
|
+
},
|
|
86
|
+
]
|
|
87
|
+
}
|
|
88
|
+
matches={matches}
|
|
89
|
+
focusable={!linked}
|
|
90
|
+
/>
|
|
49
91
|
</span>
|
|
50
92
|
</span>
|
|
51
93
|
{line.references.length > 0 && (
|
|
@@ -55,32 +97,32 @@ export function CallStackDiff(
|
|
|
55
97
|
)}
|
|
56
98
|
</>
|
|
57
99
|
);
|
|
58
|
-
|
|
59
|
-
return (
|
|
60
|
-
<div
|
|
61
|
-
key={index}
|
|
62
|
-
className={row}
|
|
63
|
-
data-change={sign}
|
|
64
|
-
title={description}
|
|
65
|
-
>
|
|
66
|
-
{content}
|
|
67
|
-
</div>
|
|
68
|
-
);
|
|
69
|
-
}
|
|
70
|
-
return (
|
|
100
|
+
const element = linked ? (
|
|
71
101
|
<button
|
|
72
102
|
key={index}
|
|
73
103
|
type="button"
|
|
74
|
-
|
|
75
|
-
data-change={sign}
|
|
104
|
+
{...rowProps}
|
|
76
105
|
aria-pressed={props.selectedAnnotation === line}
|
|
77
106
|
aria-controls="source-diff-panel"
|
|
78
|
-
title={description}
|
|
79
107
|
aria-label={description}
|
|
80
108
|
onClick={() => props.onSelectAnnotation(line)}
|
|
81
109
|
>
|
|
82
110
|
{content}
|
|
83
111
|
</button>
|
|
112
|
+
) : (
|
|
113
|
+
<div
|
|
114
|
+
key={index}
|
|
115
|
+
{...rowProps}
|
|
116
|
+
tabIndex={lineNote === undefined ? undefined : 0}
|
|
117
|
+
>
|
|
118
|
+
{content}
|
|
119
|
+
</div>
|
|
120
|
+
);
|
|
121
|
+
if (lineNote === undefined) return element;
|
|
122
|
+
return (
|
|
123
|
+
<Tooltip key={index} content={<NoteText text={lineNote.text} />}>
|
|
124
|
+
{element}
|
|
125
|
+
</Tooltip>
|
|
84
126
|
);
|
|
85
127
|
})}
|
|
86
128
|
</div>
|
|
@@ -120,6 +162,30 @@ const styles = {
|
|
|
120
162
|
border: 0,
|
|
121
163
|
"&[data-change='+']": { backgroundColor: colors.green[3] },
|
|
122
164
|
"&[data-change='-']": { backgroundColor: colors.red[3] },
|
|
165
|
+
"&[data-highlighted]": {
|
|
166
|
+
backgroundColor: colors.blue[3],
|
|
167
|
+
boxShadow: `inset 3px 0 ${colors.blue[9]}`,
|
|
168
|
+
},
|
|
169
|
+
"&[data-change='+'][data-highlighted]": {
|
|
170
|
+
backgroundColor: colors.green[5],
|
|
171
|
+
boxShadow: `inset 3px 0 ${colors.green[9]}`,
|
|
172
|
+
},
|
|
173
|
+
"&[data-change='-'][data-highlighted]": {
|
|
174
|
+
backgroundColor: colors.red[5],
|
|
175
|
+
boxShadow: `inset 3px 0 ${colors.red[9]}`,
|
|
176
|
+
},
|
|
177
|
+
"&[data-note]": { cursor: "help" },
|
|
178
|
+
// The sign fills the row gutter, so the dot follows the label instead.
|
|
179
|
+
"&[data-note='start'] .stack-label::after": {
|
|
180
|
+
content: '""',
|
|
181
|
+
display: "inline-block",
|
|
182
|
+
width: "5px",
|
|
183
|
+
height: "5px",
|
|
184
|
+
marginLeft: "8px",
|
|
185
|
+
verticalAlign: "middle",
|
|
186
|
+
borderRadius: "50%",
|
|
187
|
+
backgroundColor: colors.blue[9],
|
|
188
|
+
},
|
|
123
189
|
"&[aria-pressed]": { cursor: "pointer" },
|
|
124
190
|
"&[aria-pressed]:hover": { backgroundColor: colors.blue[2] },
|
|
125
191
|
"&[aria-pressed='true'], &[aria-pressed='true']:hover": {
|