@contraagent/elements 0.1.0 → 0.3.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 +16 -5
- package/custom-elements.json +1107 -345
- package/dist/code/code.d.ts +83 -0
- package/dist/code/code.d.ts.map +1 -0
- package/dist/code/code.js +206 -0
- package/dist/code/code.js.map +1 -0
- package/dist/code/highlight.d.ts +25 -0
- package/dist/code/highlight.d.ts.map +1 -0
- package/dist/code/highlight.js +312 -0
- package/dist/code/highlight.js.map +1 -0
- package/dist/code/index.d.ts +3 -0
- package/dist/code/index.d.ts.map +1 -0
- package/dist/code/index.js +5 -0
- package/dist/code/index.js.map +1 -0
- package/dist/define/all.d.ts +2 -0
- package/dist/define/all.d.ts.map +1 -1
- package/dist/define/all.js +2 -0
- package/dist/define/all.js.map +1 -1
- package/dist/define/code.d.ts +4 -0
- package/dist/define/code.d.ts.map +1 -0
- package/dist/define/code.js +7 -0
- package/dist/define/code.js.map +1 -0
- package/dist/define/tree.d.ts +4 -0
- package/dist/define/tree.d.ts.map +1 -0
- package/dist/define/tree.js +7 -0
- package/dist/define/tree.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/markdown/highlight.d.ts +1 -4
- package/dist/markdown/highlight.d.ts.map +1 -1
- package/dist/markdown/highlight.js +2 -85
- package/dist/markdown/highlight.js.map +1 -1
- package/dist/styles/prose.d.ts.map +1 -1
- package/dist/styles/prose.js +5 -0
- package/dist/styles/prose.js.map +1 -1
- package/dist/tree/index.d.ts +2 -0
- package/dist/tree/index.d.ts.map +1 -0
- package/dist/tree/index.js +4 -0
- package/dist/tree/index.js.map +1 -0
- package/dist/tree/tree.d.ts +83 -0
- package/dist/tree/tree.d.ts.map +1 -0
- package/dist/tree/tree.js +251 -0
- package/dist/tree/tree.js.map +1 -0
- package/docs/code.md +55 -0
- package/docs/examples/code.ts +28 -0
- package/docs/examples/tree.ts +44 -0
- package/docs/tree.md +44 -0
- package/package.json +19 -3
- package/src/code/code.ts +199 -0
- package/src/code/highlight.ts +378 -0
- package/src/code/index.ts +4 -0
- package/src/define/all.ts +2 -0
- package/src/define/code.ts +9 -0
- package/src/define/tree.ts +9 -0
- package/src/index.ts +2 -0
- package/src/markdown/highlight.ts +2 -94
- package/src/styles/prose.ts +5 -0
- package/src/tree/index.ts +3 -0
- package/src/tree/tree.ts +263 -0
|
@@ -1,94 +1,2 @@
|
|
|
1
|
-
//
|
|
2
|
-
|
|
3
|
-
// before it is wrapped, so no input can inject markup.
|
|
4
|
-
import { esc } from '../core/format.js';
|
|
5
|
-
|
|
6
|
-
const PY_KW = new Set(
|
|
7
|
-
'and as assert async await break class continue def del elif else except finally for from global if import in is lambda nonlocal not or pass raise return try while with yield'.split(
|
|
8
|
-
' ',
|
|
9
|
-
),
|
|
10
|
-
);
|
|
11
|
-
const PY_CONST = new Set(['None', 'True', 'False', 'self', 'cls']);
|
|
12
|
-
const PY_BUILTIN = new Set(
|
|
13
|
-
'str int float bool list dict set tuple bytes object type len range print isinstance super Optional Literal Any Union Path'.split(' '),
|
|
14
|
-
);
|
|
15
|
-
const PY_TOKEN = new RegExp(
|
|
16
|
-
[
|
|
17
|
-
'(#[^\\n]*)', // 1 comment
|
|
18
|
-
'((?:[rRbBfFuU]{0,2})(?:"""[\\s\\S]*?"""|\'\'\'[\\s\\S]*?\'\'\'))', // 2 triple-quoted string (docstrings)
|
|
19
|
-
'((?:[rRbBfFuU]{0,2})(?:"(?:[^"\\\\\\n]|\\\\.)*"|\'(?:[^\'\\\\\\n]|\\\\.)*\'))', // 3 string
|
|
20
|
-
'(@[A-Za-z_][\\w.]*)', // 4 decorator
|
|
21
|
-
'(\\b\\d[\\d_]*(?:\\.\\d+)?(?:[eE][+-]?\\d+)?\\b)', // 5 number
|
|
22
|
-
'([A-Za-z_]\\w*)', // 6 name
|
|
23
|
-
].join('|'),
|
|
24
|
-
'g',
|
|
25
|
-
);
|
|
26
|
-
|
|
27
|
-
export function hlPython(src: string): string {
|
|
28
|
-
let out = '';
|
|
29
|
-
let last = 0;
|
|
30
|
-
let prevWord = '';
|
|
31
|
-
PY_TOKEN.lastIndex = 0;
|
|
32
|
-
for (let m = PY_TOKEN.exec(src); m; m = PY_TOKEN.exec(src)) {
|
|
33
|
-
out += esc(src.slice(last, m.index));
|
|
34
|
-
last = PY_TOKEN.lastIndex;
|
|
35
|
-
const [tok, com, tri, str, dec, num, name] = m;
|
|
36
|
-
if (com) out += `<span class="hl-c">${esc(tok)}</span>`;
|
|
37
|
-
else if (tri) out += `<span class="hl-s hl-doc">${esc(tok)}</span>`;
|
|
38
|
-
else if (str) out += `<span class="hl-s">${esc(tok)}</span>`;
|
|
39
|
-
else if (dec) out += `<span class="hl-d">${esc(tok)}</span>`;
|
|
40
|
-
else if (num) out += `<span class="hl-n">${esc(tok)}</span>`;
|
|
41
|
-
else if (name) {
|
|
42
|
-
const cls = PY_KW.has(name)
|
|
43
|
-
? 'hl-k'
|
|
44
|
-
: PY_CONST.has(name)
|
|
45
|
-
? 'hl-k hl-const'
|
|
46
|
-
: prevWord === 'class'
|
|
47
|
-
? 'hl-cls'
|
|
48
|
-
: prevWord === 'def'
|
|
49
|
-
? 'hl-f'
|
|
50
|
-
: PY_BUILTIN.has(name)
|
|
51
|
-
? 'hl-b'
|
|
52
|
-
: /^[A-Z]\w*$/.test(name) && name.length > 1 && /[a-z]/.test(name)
|
|
53
|
-
? 'hl-t'
|
|
54
|
-
: '';
|
|
55
|
-
out += cls ? `<span class="${cls}">${esc(name)}</span>` : esc(name);
|
|
56
|
-
prevWord = name;
|
|
57
|
-
continue;
|
|
58
|
-
}
|
|
59
|
-
if (!/^\s*$/.test(tok)) prevWord = '';
|
|
60
|
-
}
|
|
61
|
-
return out + esc(src.slice(last));
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
export function hlJson(src: string): string {
|
|
65
|
-
const re = /("(?:[^"\\\n]|\\.)*")(\s*:)?|\b(true|false|null)\b|(-?\b\d+(?:\.\d+)?(?:[eE][+-]?\d+)?\b)/g;
|
|
66
|
-
let out = '';
|
|
67
|
-
let last = 0;
|
|
68
|
-
for (let m = re.exec(src); m; m = re.exec(src)) {
|
|
69
|
-
out += esc(src.slice(last, m.index));
|
|
70
|
-
last = re.lastIndex;
|
|
71
|
-
const [, str, colon, kw, num] = m;
|
|
72
|
-
out += str
|
|
73
|
-
? colon
|
|
74
|
-
? `<span class="hl-key">${esc(str)}</span>${esc(colon)}`
|
|
75
|
-
: `<span class="hl-s">${esc(str)}</span>`
|
|
76
|
-
: kw
|
|
77
|
-
? `<span class="hl-k hl-const">${kw}</span>`
|
|
78
|
-
: `<span class="hl-n">${num ?? ''}</span>`;
|
|
79
|
-
}
|
|
80
|
-
return out + esc(src.slice(last));
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
const LANG_OF: Record<string, 'python' | 'json'> = { py: 'python', python: 'python', json: 'json', jsonc: 'json' };
|
|
84
|
-
|
|
85
|
-
function langOf(hint: string | null | undefined): 'python' | 'json' | '' {
|
|
86
|
-
const h = String(hint || '').toLowerCase();
|
|
87
|
-
return LANG_OF[h.replace(/^.*\./, '')] || LANG_OF[h] || '';
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
/** Highlighted, escaped HTML. `hint` is a fence language ("python") or a file name ("schema.py"); anything unknown is plain. */
|
|
91
|
-
export function highlight(code: string, hint?: string | null): string {
|
|
92
|
-
const lang = langOf(hint);
|
|
93
|
-
return lang === 'python' ? hlPython(code) : lang === 'json' ? hlJson(code) : esc(code);
|
|
94
|
-
}
|
|
1
|
+
// The highlighter of markdown's code blocks: `code/highlight.ts`, by the names of this entry.
|
|
2
|
+
export { highlight, hlJson, hlPython } from '../code/highlight.js';
|
package/src/styles/prose.ts
CHANGED
|
@@ -56,4 +56,9 @@ export const prose = css`
|
|
|
56
56
|
.hl-t { color: var(--code-type); }
|
|
57
57
|
.hl-b { color: var(--code-name); }
|
|
58
58
|
.hl-key { color: var(--code-name); }
|
|
59
|
+
.hl-tag { color: var(--code-keyword); }
|
|
60
|
+
.hl-attr { color: var(--code-name); }
|
|
61
|
+
.hl-meta { color: var(--code-comment); font-weight: 600; }
|
|
62
|
+
.hl-ins { color: var(--status-done); }
|
|
63
|
+
.hl-del { color: var(--status-failed); }
|
|
59
64
|
`;
|
package/src/tree/tree.ts
ADDED
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
import { LitElement, css, html, nothing, svg, type PropertyValues, type TemplateResult } from 'lit';
|
|
2
|
+
import { property, state } from 'lit/decorators.js';
|
|
3
|
+
import { emit } from '../core/events.js';
|
|
4
|
+
import { withDefaults } from '../core/strings.js';
|
|
5
|
+
import type { Tone } from '../core/tone.js';
|
|
6
|
+
import { statusChip } from '../primitives/pill.js';
|
|
7
|
+
import { base } from '../styles/index.js';
|
|
8
|
+
|
|
9
|
+
/** One item of a tree: its name, the items under it, and how it looks. */
|
|
10
|
+
export interface TreeItem {
|
|
11
|
+
/** Its id, unique in the tree. */
|
|
12
|
+
id: string;
|
|
13
|
+
name: string;
|
|
14
|
+
/** Its icon: a directory for an item with children, a file for one without, unless given. */
|
|
15
|
+
icon?: 'file' | 'directory' | null;
|
|
16
|
+
/** A word beside the name, coloured by `tone` (with its mark: ✓ done, ✕ failed, ◌ attention). */
|
|
17
|
+
badge?: string | null;
|
|
18
|
+
tone?: Tone | null;
|
|
19
|
+
/** Its children are shown at first (default: true). */
|
|
20
|
+
open?: boolean | null;
|
|
21
|
+
children?: readonly TreeItem[] | null;
|
|
22
|
+
/** A click or Enter selects it (`cae-select`); default: an item with no children. An item with children that is not selectable opens and closes instead. */
|
|
23
|
+
selectable?: boolean | null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface TreeStrings {
|
|
27
|
+
/** What a tree with no items shows. */
|
|
28
|
+
empty: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export const DEFAULT_TREE_STRINGS: Readonly<TreeStrings> = { empty: 'No items' };
|
|
32
|
+
|
|
33
|
+
/** An item where the tree shows it: its depth (1 at the top), its parent, and its place among its siblings. */
|
|
34
|
+
export interface TreeRow {
|
|
35
|
+
item: TreeItem;
|
|
36
|
+
level: number;
|
|
37
|
+
parent: string | null;
|
|
38
|
+
index: number;
|
|
39
|
+
siblings: number;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const hasChildren = (t: TreeItem): boolean => !!t.children?.length;
|
|
43
|
+
const selectable = (t: TreeItem): boolean => t.selectable ?? !hasChildren(t);
|
|
44
|
+
|
|
45
|
+
/** The items shown, top to bottom: every item whose ancestors are all open (`isOpen`). */
|
|
46
|
+
export function visibleRows(items: readonly TreeItem[], isOpen: (t: TreeItem) => boolean): TreeRow[] {
|
|
47
|
+
const out: TreeRow[] = [];
|
|
48
|
+
const walk = (list: readonly TreeItem[], level: number, parent: string | null) =>
|
|
49
|
+
list.forEach((item, index) => {
|
|
50
|
+
out.push({ item, level, parent, index, siblings: list.length });
|
|
51
|
+
if (hasChildren(item) && isOpen(item)) walk(item.children!, level + 1, item.id);
|
|
52
|
+
});
|
|
53
|
+
walk(items, 1, null);
|
|
54
|
+
return out;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** The ids of the ancestors of item `id`, outermost first; null when no item has that id. */
|
|
58
|
+
export function ancestorsOf(items: readonly TreeItem[], id: string): string[] | null {
|
|
59
|
+
for (const t of items) {
|
|
60
|
+
if (t.id === id) return [];
|
|
61
|
+
const below = t.children ? ancestorsOf(t.children, id) : null;
|
|
62
|
+
if (below) return [t.id, ...below];
|
|
63
|
+
}
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** The first item, depth first, that `match` accepts. */
|
|
68
|
+
export function findItem(items: readonly TreeItem[], match: (t: TreeItem) => boolean): TreeItem | null {
|
|
69
|
+
for (const t of items) {
|
|
70
|
+
if (match(t)) return t;
|
|
71
|
+
const below = t.children ? findItem(t.children, match) : null;
|
|
72
|
+
if (below) return below;
|
|
73
|
+
}
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const FOLDER = svg`<path d="M1.5 3.5h4l1.5 1.5h7.5v8h-13z" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linejoin="round"></path>`;
|
|
78
|
+
const FILE = svg`<path d="M3.5 1.5h6l3 3v10h-9z M9.5 1.5v3h3" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linejoin="round"></path>`;
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Items in a tree, the children of each under it, folded and opened by the reader: files
|
|
82
|
+
* and directories, say. It is one tab stop: Up and Down move between the items shown,
|
|
83
|
+
* Right opens an item (then moves to its first child), Left closes it (or moves to its
|
|
84
|
+
* parent), Home and End go to the first and the last, a letter to the next item of
|
|
85
|
+
* that initial; Enter, Space or a click selects a selectable item (`cae-select`) and
|
|
86
|
+
* opens or closes any other. The toggle before an item with children opens and closes
|
|
87
|
+
* it (`cae-toggle`). The ancestors of `selected` are always open.
|
|
88
|
+
*
|
|
89
|
+
* @tagname cae-tree
|
|
90
|
+
* @fires cae-select - `{id}`: an item was chosen with a click, Enter or Space
|
|
91
|
+
* @fires cae-toggle - `{id, open}`: the reader opened or closed an item
|
|
92
|
+
* @csspart tree - the list of items
|
|
93
|
+
* @csspart item - an item's row
|
|
94
|
+
* @csspart empty - what it shows with no items
|
|
95
|
+
*/
|
|
96
|
+
export class CaeTree extends LitElement {
|
|
97
|
+
static override styles = [
|
|
98
|
+
base,
|
|
99
|
+
css`
|
|
100
|
+
:host { display: block; font-size: var(--font-size-md); }
|
|
101
|
+
.tree { margin: 0; padding: 2px 0; }
|
|
102
|
+
.row { display: flex; align-items: center; gap: 4px; min-height: 24px; padding: 0 8px 0 calc((var(--level) - 1) * 14px + 4px);
|
|
103
|
+
border-radius: var(--radius-sm); cursor: pointer; white-space: nowrap; color: var(--text-2); }
|
|
104
|
+
.row:hover { background: var(--raise); }
|
|
105
|
+
.row[aria-selected='true'] { background: color-mix(in srgb, var(--accent) 16%, transparent); color: var(--text); box-shadow: inset 2px 0 0 var(--accent); }
|
|
106
|
+
.row:focus-visible { outline-offset: -2px; }
|
|
107
|
+
.twist { flex: none; display: inline-grid; place-items: center; width: 16px; height: 16px; color: var(--text-muted); border-radius: var(--radius-sm); }
|
|
108
|
+
.twist svg { transition: transform 0.12s ease; }
|
|
109
|
+
[aria-expanded='true'] > .twist svg { transform: rotate(90deg); }
|
|
110
|
+
.twist:hover { color: var(--text); }
|
|
111
|
+
.icon { flex: none; width: 16px; height: 16px; color: var(--text-muted); }
|
|
112
|
+
.name { overflow: hidden; text-overflow: ellipsis; min-width: 0; }
|
|
113
|
+
.badge { flex: none; margin-left: 6px; font-size: 11px; padding: 0 6px; border-radius: var(--radius-pill); background: var(--raise); color: var(--text-2); }
|
|
114
|
+
.badge[data-tone] { background: var(--tone-bg); color: var(--tone); }
|
|
115
|
+
.badge[data-tone='done'] { background: none; color: var(--text-2); }
|
|
116
|
+
.empty { color: var(--text-muted); font-size: var(--font-size-sm); padding: 4px 8px; }
|
|
117
|
+
@media (prefers-reduced-motion: reduce) { .twist svg { transition: none; } }
|
|
118
|
+
`,
|
|
119
|
+
];
|
|
120
|
+
|
|
121
|
+
@property({ attribute: false }) items: readonly TreeItem[] = [];
|
|
122
|
+
/** The selected item's id, or null. */
|
|
123
|
+
@property({ attribute: false }) selected: string | null = null;
|
|
124
|
+
/** What the tree is named to a screen reader. */
|
|
125
|
+
@property() label = '';
|
|
126
|
+
/** The visible words; English by default. */
|
|
127
|
+
@property({ attribute: false }) strings: Partial<TreeStrings> = {};
|
|
128
|
+
|
|
129
|
+
/** The reader's choice of open and closed items, by id, over each item's `open`. */
|
|
130
|
+
@state() private opened = new Map<string, boolean>();
|
|
131
|
+
/** The item that holds the tab stop. */
|
|
132
|
+
@state() private focusId: string | null = null;
|
|
133
|
+
|
|
134
|
+
private readonly isOpen = (t: TreeItem): boolean => this.opened.get(t.id) ?? t.open ?? true;
|
|
135
|
+
|
|
136
|
+
override willUpdate(changed: PropertyValues<this>): void {
|
|
137
|
+
if (changed.has('selected') && this.selected) {
|
|
138
|
+
const up = ancestorsOf(this.items, this.selected);
|
|
139
|
+
if (up?.some((id) => this.opened.get(id) !== true)) {
|
|
140
|
+
const next = new Map(this.opened);
|
|
141
|
+
for (const id of up) next.set(id, true);
|
|
142
|
+
this.opened = next;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
override updated(changed: PropertyValues<this>): void {
|
|
148
|
+
if (changed.has('selected') && this.selected && !this.matches(':focus-within')) {
|
|
149
|
+
this.rowOf(this.selected)?.scrollIntoView?.({ block: 'nearest' });
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
private rowOf(id: string): HTMLElement | null {
|
|
154
|
+
return this.renderRoot.querySelector<HTMLElement>(`[data-id="${CSS.escape(id)}"]`);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
private setOpen(t: TreeItem, open: boolean): void {
|
|
158
|
+
if (!hasChildren(t) || this.isOpen(t) === open) return;
|
|
159
|
+
this.opened = new Map(this.opened).set(t.id, open);
|
|
160
|
+
emit(this, 'cae-toggle', { id: t.id, open });
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
private choose(t: TreeItem): void {
|
|
164
|
+
this.focusId = t.id;
|
|
165
|
+
if (selectable(t)) emit(this, 'cae-select', { id: t.id });
|
|
166
|
+
else this.setOpen(t, !this.isOpen(t));
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
private async moveTo(id: string): Promise<void> {
|
|
170
|
+
this.focusId = id;
|
|
171
|
+
await this.updateComplete;
|
|
172
|
+
this.rowOf(id)?.focus();
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
private onKey(e: KeyboardEvent, rows: readonly TreeRow[], i: number): void {
|
|
176
|
+
const row = rows[i]!;
|
|
177
|
+
const t = row.item;
|
|
178
|
+
let to: TreeRow | undefined;
|
|
179
|
+
switch (e.key) {
|
|
180
|
+
case 'ArrowDown':
|
|
181
|
+
to = rows[i + 1];
|
|
182
|
+
break;
|
|
183
|
+
case 'ArrowUp':
|
|
184
|
+
to = rows[i - 1];
|
|
185
|
+
break;
|
|
186
|
+
case 'Home':
|
|
187
|
+
to = rows[0];
|
|
188
|
+
break;
|
|
189
|
+
case 'End':
|
|
190
|
+
to = rows[rows.length - 1];
|
|
191
|
+
break;
|
|
192
|
+
case 'ArrowRight':
|
|
193
|
+
if (hasChildren(t) && !this.isOpen(t)) this.setOpen(t, true);
|
|
194
|
+
else if (hasChildren(t)) to = rows[i + 1];
|
|
195
|
+
break;
|
|
196
|
+
case 'ArrowLeft':
|
|
197
|
+
if (hasChildren(t) && this.isOpen(t)) this.setOpen(t, false);
|
|
198
|
+
else if (row.parent) to = rows.find((r) => r.item.id === row.parent);
|
|
199
|
+
break;
|
|
200
|
+
case 'Enter':
|
|
201
|
+
case ' ':
|
|
202
|
+
this.choose(t);
|
|
203
|
+
break;
|
|
204
|
+
default: {
|
|
205
|
+
if (e.key.length !== 1 || e.ctrlKey || e.metaKey || e.altKey || !/\S/.test(e.key)) return;
|
|
206
|
+
const k = e.key.toLowerCase();
|
|
207
|
+
const order = [...rows.slice(i + 1), ...rows.slice(0, i)];
|
|
208
|
+
to = order.find((r) => r.item.name.toLowerCase().startsWith(k));
|
|
209
|
+
if (!to) return;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
e.preventDefault();
|
|
213
|
+
if (to) void this.moveTo(to.item.id);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
override render() {
|
|
217
|
+
if (!this.items.length) return html`<div class="empty" part="empty">${withDefaults(DEFAULT_TREE_STRINGS, this.strings).empty}</div>`;
|
|
218
|
+
const rows = visibleRows(this.items, this.isOpen);
|
|
219
|
+
const stop = rows.find((r) => r.item.id === this.focusId) ?? rows.find((r) => r.item.id === this.selected) ?? rows[0];
|
|
220
|
+
return html`<div class="tree" role="tree" part="tree" aria-label=${this.label || nothing}>
|
|
221
|
+
${rows.map((r, i) => this.row(r, i, rows, r === stop))}
|
|
222
|
+
</div>`;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
private row(r: TreeRow, i: number, rows: readonly TreeRow[], stop: boolean): TemplateResult {
|
|
226
|
+
const t = r.item;
|
|
227
|
+
const parent = hasChildren(t);
|
|
228
|
+
const open = parent && this.isOpen(t);
|
|
229
|
+
const directory = (t.icon ?? (parent ? 'directory' : 'file')) === 'directory';
|
|
230
|
+
return html`<div
|
|
231
|
+
class="row"
|
|
232
|
+
part="item"
|
|
233
|
+
role="treeitem"
|
|
234
|
+
data-id=${t.id}
|
|
235
|
+
tabindex=${stop ? 0 : -1}
|
|
236
|
+
style="--level: ${r.level}"
|
|
237
|
+
aria-level=${r.level}
|
|
238
|
+
aria-setsize=${r.siblings}
|
|
239
|
+
aria-posinset=${r.index + 1}
|
|
240
|
+
aria-expanded=${parent ? String(open) : nothing}
|
|
241
|
+
aria-selected=${selectable(t) ? String(t.id === this.selected) : nothing}
|
|
242
|
+
@click=${() => this.choose(t)}
|
|
243
|
+
@focus=${() => (this.focusId = t.id)}
|
|
244
|
+
@keydown=${(e: KeyboardEvent) => this.onKey(e, rows, i)}
|
|
245
|
+
>
|
|
246
|
+
<span class="twist" aria-hidden="true" @click=${(e: Event) => {
|
|
247
|
+
if (!parent) return;
|
|
248
|
+
e.stopPropagation();
|
|
249
|
+
this.focusId = t.id;
|
|
250
|
+
this.setOpen(t, !open);
|
|
251
|
+
}}>${parent ? html`<svg width="10" height="10" viewBox="0 0 10 10"><path d="M3 1.5 7 5 3 8.5" fill="none" stroke="currentColor" stroke-width="1.5"></path></svg>` : nothing}</span>
|
|
252
|
+
<svg class="icon" viewBox="0 0 16 16" aria-hidden="true">${directory ? FOLDER : FILE}</svg>
|
|
253
|
+
<span class="name">${t.name}</span>
|
|
254
|
+
${t.badge ? statusChip('badge', { text: t.badge, tone: t.tone ?? 'neutral' }) : nothing}
|
|
255
|
+
</div>`;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
declare global {
|
|
260
|
+
interface HTMLElementTagNameMap {
|
|
261
|
+
'cae-tree': CaeTree;
|
|
262
|
+
}
|
|
263
|
+
}
|