@erclx/canon 5.15.0 → 5.16.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/claude/.claude-plugin/plugin.json +1 -1
- package/claude/skills/draft-wiki/SKILL.md +1 -1
- package/claude/skills/plan-groundwork/SKILL.md +1 -1
- package/claude/skills/search-craft/REQUIREMENT.md +57 -0
- package/claude/skills/search-craft/SKILL.md +83 -0
- package/docs/agents/canvas.md +36 -0
- package/docs/agents/commands.md +3 -3
- package/docs/agents/index.md +1 -0
- package/docs/workflow/skill-map.md +1 -0
- package/package.json +1 -1
- package/scripts/lib/sandbox-git.sh +2 -2
- package/src/canvas/address.ts +184 -0
- package/src/canvas/capture.ts +172 -0
- package/src/canvas/client/app.tsx +2 -0
- package/src/canvas/client/inspector.tsx +121 -0
- package/src/canvas/client/layers.tsx +118 -0
- package/src/canvas/client/pages-panel.tsx +47 -13
- package/src/canvas/client/shell.css +131 -1
- package/src/canvas/client/state.ts +186 -0
- package/src/canvas/client/surface.tsx +272 -20
- package/src/canvas/content.ts +307 -16
- package/src/canvas/server.ts +182 -13
- package/src/claude/cases/misc.ts +5 -0
- package/src/commands/canvas.ts +200 -3
- package/src/commands/migrate.ts +1 -1
- package/src/migrate/rename.ts +5 -4
- package/src/teach/browser/compile.ts +44 -0
- package/src/teach/browser/dismiss.ts +76 -0
- package/src/teach/browser/focus-line.ts +19 -0
- package/src/teach/browser/glossary-filter.ts +44 -0
- package/src/teach/browser/head.ts +30 -0
- package/src/teach/browser/quiz.ts +19 -0
- package/src/teach/browser/sidebar.ts +233 -0
- package/src/teach/browser/theme.ts +18 -0
- package/src/teach/nav.ts +19 -374
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/** @jsxImportSource preact */
|
|
2
2
|
import type { JSX } from 'preact'
|
|
3
3
|
import { useRef } from 'preact/hooks'
|
|
4
|
+
import { Inspector } from '@/canvas/client/inspector'
|
|
4
5
|
import { PagesPanel } from '@/canvas/client/pages-panel'
|
|
5
6
|
import { currentPage, focusFrame, tokens } from '@/canvas/client/state'
|
|
6
7
|
import { Surface } from '@/canvas/client/surface'
|
|
@@ -77,6 +78,7 @@ export function App(): JSX.Element {
|
|
|
77
78
|
/>
|
|
78
79
|
<Surface viewportRef={viewportRef} />
|
|
79
80
|
<aside class="panel panel-right" aria-label="Details">
|
|
81
|
+
<Inspector />
|
|
80
82
|
<PageDetails />
|
|
81
83
|
<TokenDetails />
|
|
82
84
|
</aside>
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/** @jsxImportSource preact */
|
|
2
|
+
import type { JSX } from 'preact'
|
|
3
|
+
import { elementAt } from '@/canvas/address'
|
|
4
|
+
import {
|
|
5
|
+
currentPage,
|
|
6
|
+
frameDocuments,
|
|
7
|
+
frameKey,
|
|
8
|
+
selectedElement,
|
|
9
|
+
selectedFrame,
|
|
10
|
+
writeError,
|
|
11
|
+
} from '@/canvas/client/state'
|
|
12
|
+
|
|
13
|
+
const FIELDS = ['x', 'y', 'width', 'height'] as const
|
|
14
|
+
|
|
15
|
+
interface Row {
|
|
16
|
+
readonly name: string
|
|
17
|
+
readonly value: string
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* The computed values the browser settled on, which is what the operator sees
|
|
22
|
+
* rather than what any one stylesheet states.
|
|
23
|
+
*/
|
|
24
|
+
function styleRows(element: Element): Row[] {
|
|
25
|
+
const rect = element.getBoundingClientRect()
|
|
26
|
+
const view = element.ownerDocument.defaultView
|
|
27
|
+
const style = view?.getComputedStyle(element)
|
|
28
|
+
const rows: Row[] = [
|
|
29
|
+
{ name: 'x', value: String(Math.round(rect.left)) },
|
|
30
|
+
{ name: 'y', value: String(Math.round(rect.top)) },
|
|
31
|
+
{ name: 'width', value: String(Math.round(rect.width)) },
|
|
32
|
+
{ name: 'height', value: String(Math.round(rect.height)) },
|
|
33
|
+
]
|
|
34
|
+
if (!style) return rows
|
|
35
|
+
return [
|
|
36
|
+
...rows,
|
|
37
|
+
{ name: 'color', value: style.color },
|
|
38
|
+
{ name: 'background', value: style.backgroundColor },
|
|
39
|
+
{ name: 'font', value: style.fontFamily },
|
|
40
|
+
{ name: 'size', value: style.fontSize },
|
|
41
|
+
{ name: 'weight', value: style.fontWeight },
|
|
42
|
+
]
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function elementName(element: Element): string {
|
|
46
|
+
const tag = element.tagName.toLowerCase()
|
|
47
|
+
return [tag, ...element.classList].join('.')
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Read-only in this slice, since editing an element is the edit slice's work. */
|
|
51
|
+
function ElementDetails(): JSX.Element | null {
|
|
52
|
+
const picked = selectedElement.value
|
|
53
|
+
const page = currentPage.value
|
|
54
|
+
if (!picked || !page) return null
|
|
55
|
+
const { frame, element } = picked
|
|
56
|
+
const doc = frameDocuments.value.get(frameKey(page.name, frame))
|
|
57
|
+
const node = doc && !element.stale ? elementAt(doc, element) : undefined
|
|
58
|
+
|
|
59
|
+
return (
|
|
60
|
+
<section aria-label="Element">
|
|
61
|
+
<h2 class="section-label">Element</h2>
|
|
62
|
+
{element.stale ? (
|
|
63
|
+
<p class="notice">
|
|
64
|
+
The frame changed since this element was picked, so its index may name
|
|
65
|
+
another element now. Pick it again.
|
|
66
|
+
</p>
|
|
67
|
+
) : node ? (
|
|
68
|
+
<>
|
|
69
|
+
<p class="detail" title={elementName(node)}>
|
|
70
|
+
{elementName(node)}
|
|
71
|
+
</p>
|
|
72
|
+
<dl class="box styles">
|
|
73
|
+
{styleRows(node).map((row) => (
|
|
74
|
+
<div key={row.name} class="box-row">
|
|
75
|
+
<dt>{row.name}</dt>
|
|
76
|
+
<dd title={row.value}>{row.value}</dd>
|
|
77
|
+
</div>
|
|
78
|
+
))}
|
|
79
|
+
</dl>
|
|
80
|
+
</>
|
|
81
|
+
) : (
|
|
82
|
+
<p class="empty">Loading the frame to read this element</p>
|
|
83
|
+
)}
|
|
84
|
+
</section>
|
|
85
|
+
)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Read-only in this slice, since editing a box is the edit slice's work. */
|
|
89
|
+
export function Inspector(): JSX.Element {
|
|
90
|
+
const frame = selectedFrame.value
|
|
91
|
+
return (
|
|
92
|
+
<>
|
|
93
|
+
<section aria-label="Frame">
|
|
94
|
+
<h2 class="section-label">Frame</h2>
|
|
95
|
+
{frame ? (
|
|
96
|
+
<>
|
|
97
|
+
<p class="detail" title={frame.name}>
|
|
98
|
+
{frame.name}
|
|
99
|
+
</p>
|
|
100
|
+
<dl class="box">
|
|
101
|
+
{FIELDS.map((field) => (
|
|
102
|
+
<div key={field} class="box-row">
|
|
103
|
+
<dt>{field}</dt>
|
|
104
|
+
<dd>{frame[field]}</dd>
|
|
105
|
+
</div>
|
|
106
|
+
))}
|
|
107
|
+
</dl>
|
|
108
|
+
</>
|
|
109
|
+
) : (
|
|
110
|
+
<p class="empty">Select a frame to see its box</p>
|
|
111
|
+
)}
|
|
112
|
+
{writeError.value ? (
|
|
113
|
+
<p class="notice" role="alert">
|
|
114
|
+
{writeError.value}
|
|
115
|
+
</p>
|
|
116
|
+
) : null}
|
|
117
|
+
</section>
|
|
118
|
+
<ElementDetails />
|
|
119
|
+
</>
|
|
120
|
+
)
|
|
121
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/** @jsxImportSource preact */
|
|
2
|
+
import type { JSX } from 'preact'
|
|
3
|
+
import { addressOf, documentElements, excerpt } from '@/canvas/address'
|
|
4
|
+
import {
|
|
5
|
+
type FrameRef,
|
|
6
|
+
frameDocuments,
|
|
7
|
+
hoveredElement,
|
|
8
|
+
selectElement,
|
|
9
|
+
selection,
|
|
10
|
+
} from '@/canvas/client/state'
|
|
11
|
+
|
|
12
|
+
/** Elements that never render, so a layer for one points at nothing. */
|
|
13
|
+
const HIDDEN = new Set(['script', 'style', 'template', 'noscript'])
|
|
14
|
+
|
|
15
|
+
const LABEL_TEXT = 24
|
|
16
|
+
|
|
17
|
+
export interface LayersProps {
|
|
18
|
+
readonly frame: FrameRef
|
|
19
|
+
/** The frame's key, which its loaded document is registered under. */
|
|
20
|
+
readonly frameKey: string
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** `tag.first-class`, then a short excerpt of a leaf's own text. */
|
|
24
|
+
function layerLabel(element: Element): { name: string; text: string } {
|
|
25
|
+
const tag = element.tagName.toLowerCase()
|
|
26
|
+
const first = element.classList[0]
|
|
27
|
+
const text =
|
|
28
|
+
element.children.length === 0
|
|
29
|
+
? excerpt(element.textContent ?? '').slice(0, LABEL_TEXT)
|
|
30
|
+
: ''
|
|
31
|
+
return { name: first ? `${tag}.${first}` : tag, text }
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function LayerNode({
|
|
35
|
+
doc,
|
|
36
|
+
element,
|
|
37
|
+
indexes,
|
|
38
|
+
props,
|
|
39
|
+
}: {
|
|
40
|
+
readonly doc: Document
|
|
41
|
+
readonly element: Element
|
|
42
|
+
readonly indexes: ReadonlyMap<Element, number>
|
|
43
|
+
readonly props: LayersProps
|
|
44
|
+
}): JSX.Element {
|
|
45
|
+
const index = indexes.get(element) ?? -1
|
|
46
|
+
const chosen = selection.value
|
|
47
|
+
const isCurrent =
|
|
48
|
+
chosen?.page === props.frame.page &&
|
|
49
|
+
chosen.frame === props.frame.frame &&
|
|
50
|
+
chosen.element?.index === index &&
|
|
51
|
+
!chosen.element.stale
|
|
52
|
+
const { name, text } = layerLabel(element)
|
|
53
|
+
const children = [...element.children].filter(
|
|
54
|
+
(child) => !HIDDEN.has(child.tagName.toLowerCase()),
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
return (
|
|
58
|
+
<li>
|
|
59
|
+
<button
|
|
60
|
+
type="button"
|
|
61
|
+
class="row layer"
|
|
62
|
+
title={text ? `${name} ${text}` : name}
|
|
63
|
+
aria-current={isCurrent ? 'true' : undefined}
|
|
64
|
+
onClick={() => {
|
|
65
|
+
const address = addressOf(doc, element)
|
|
66
|
+
if (address) void selectElement(props.frame, props.frameKey, address)
|
|
67
|
+
}}
|
|
68
|
+
onMouseEnter={() => {
|
|
69
|
+
hoveredElement.value = { key: props.frameKey, index }
|
|
70
|
+
}}
|
|
71
|
+
onMouseLeave={() => {
|
|
72
|
+
hoveredElement.value = undefined
|
|
73
|
+
}}
|
|
74
|
+
>
|
|
75
|
+
<span class="row-label">{name}</span>
|
|
76
|
+
{text ? <span class="row-meta layer-text">{text}</span> : null}
|
|
77
|
+
</button>
|
|
78
|
+
{children.length > 0 ? (
|
|
79
|
+
<ul class="layers">
|
|
80
|
+
{children.map((child, at) => (
|
|
81
|
+
<LayerNode
|
|
82
|
+
key={at}
|
|
83
|
+
doc={doc}
|
|
84
|
+
element={child}
|
|
85
|
+
indexes={indexes}
|
|
86
|
+
props={props}
|
|
87
|
+
/>
|
|
88
|
+
))}
|
|
89
|
+
</ul>
|
|
90
|
+
) : null}
|
|
91
|
+
</li>
|
|
92
|
+
)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* The element tree of one frame, read from its loaded document, so it shows
|
|
97
|
+
* what the browser built rather than what the file states. Each row selects by
|
|
98
|
+
* the same address a click on the surface records.
|
|
99
|
+
*/
|
|
100
|
+
export function Layers(props: LayersProps): JSX.Element {
|
|
101
|
+
const doc = frameDocuments.value.get(props.frameKey)
|
|
102
|
+
const label = `Layers of ${props.frame.frame}`
|
|
103
|
+
if (!doc?.body) {
|
|
104
|
+
return (
|
|
105
|
+
<p class="empty" aria-label={label}>
|
|
106
|
+
Loading layers
|
|
107
|
+
</p>
|
|
108
|
+
)
|
|
109
|
+
}
|
|
110
|
+
const indexes = new Map(
|
|
111
|
+
documentElements(doc).map((element, index) => [element, index]),
|
|
112
|
+
)
|
|
113
|
+
return (
|
|
114
|
+
<ul class="layers layers-root" aria-label={label}>
|
|
115
|
+
<LayerNode doc={doc} element={doc.body} indexes={indexes} props={props} />
|
|
116
|
+
</ul>
|
|
117
|
+
)
|
|
118
|
+
}
|
|
@@ -1,13 +1,19 @@
|
|
|
1
1
|
/** @jsxImportSource preact */
|
|
2
2
|
import type { JSX } from 'preact'
|
|
3
|
+
import { Layers } from '@/canvas/client/layers'
|
|
3
4
|
import type { Frame } from '@/canvas/content'
|
|
4
5
|
import {
|
|
5
6
|
currentPage,
|
|
7
|
+
expandedFrames,
|
|
8
|
+
frameKey,
|
|
6
9
|
isLoaded,
|
|
7
10
|
loadError,
|
|
8
11
|
pages,
|
|
12
|
+
selectedFrame,
|
|
9
13
|
selectedPage,
|
|
14
|
+
selectFrame,
|
|
10
15
|
theme,
|
|
16
|
+
toggleLayers,
|
|
11
17
|
toggleTheme,
|
|
12
18
|
} from '@/canvas/client/state'
|
|
13
19
|
|
|
@@ -76,19 +82,47 @@ function FrameList({ onFocusFrame }: PagesPanelProps): JSX.Element | null {
|
|
|
76
82
|
}
|
|
77
83
|
return (
|
|
78
84
|
<ul class="list" aria-label="Frames">
|
|
79
|
-
{page.frames.map((frame) =>
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
85
|
+
{page.frames.map((frame) => {
|
|
86
|
+
const key = frameKey(page.name, frame)
|
|
87
|
+
const isOpen = expandedFrames.value.has(key)
|
|
88
|
+
return (
|
|
89
|
+
<li key={frame.name}>
|
|
90
|
+
<div class="frame-row">
|
|
91
|
+
<button
|
|
92
|
+
type="button"
|
|
93
|
+
class="disclosure"
|
|
94
|
+
aria-expanded={isOpen}
|
|
95
|
+
aria-label={`${isOpen ? 'Hide' : 'Show'} layers of ${frame.name}`}
|
|
96
|
+
title={`${isOpen ? 'Hide' : 'Show'} layers`}
|
|
97
|
+
onClick={() => toggleLayers(key)}
|
|
98
|
+
>
|
|
99
|
+
<span aria-hidden="true">{isOpen ? '▾' : '▸'}</span>
|
|
100
|
+
</button>
|
|
101
|
+
<button
|
|
102
|
+
type="button"
|
|
103
|
+
class="row"
|
|
104
|
+
title={frame.name}
|
|
105
|
+
aria-current={
|
|
106
|
+
frame.name === selectedFrame.value?.name ? 'true' : undefined
|
|
107
|
+
}
|
|
108
|
+
onClick={() => {
|
|
109
|
+
onFocusFrame(frame)
|
|
110
|
+
void selectFrame({ page: page.name, frame: frame.name })
|
|
111
|
+
}}
|
|
112
|
+
>
|
|
113
|
+
<span class="row-label">{frame.name}</span>
|
|
114
|
+
<span class="row-meta">{frame.width}</span>
|
|
115
|
+
</button>
|
|
116
|
+
</div>
|
|
117
|
+
{isOpen ? (
|
|
118
|
+
<Layers
|
|
119
|
+
frame={{ page: page.name, frame: frame.name }}
|
|
120
|
+
frameKey={key}
|
|
121
|
+
/>
|
|
122
|
+
) : null}
|
|
123
|
+
</li>
|
|
124
|
+
)
|
|
125
|
+
})}
|
|
92
126
|
</ul>
|
|
93
127
|
)
|
|
94
128
|
}
|
|
@@ -119,7 +119,8 @@ code {
|
|
|
119
119
|
background: var(--color-border);
|
|
120
120
|
}
|
|
121
121
|
|
|
122
|
-
.row[aria-current='page']
|
|
122
|
+
.row[aria-current='page'],
|
|
123
|
+
.row[aria-current='true'] {
|
|
123
124
|
background: var(--color-surface);
|
|
124
125
|
color: var(--color-text);
|
|
125
126
|
box-shadow: inset 2px 0 0 var(--color-accent);
|
|
@@ -275,6 +276,73 @@ code {
|
|
|
275
276
|
border-radius: 2px;
|
|
276
277
|
}
|
|
277
278
|
|
|
279
|
+
/* The frame's handle, since a press inside the frame picks an element. */
|
|
280
|
+
.frame-label {
|
|
281
|
+
cursor: move;
|
|
282
|
+
touch-action: none;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
.frame[data-dragging='true'] .frame-label {
|
|
286
|
+
cursor: grabbing;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
.frame-body {
|
|
290
|
+
position: relative;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/* Inset by the iframe border, so a box lines up with the element it marks. */
|
|
294
|
+
.element-outline {
|
|
295
|
+
position: absolute;
|
|
296
|
+
translate: 1px 1px;
|
|
297
|
+
pointer-events: none;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
.element-outline[data-outline='hover'] {
|
|
301
|
+
outline: calc(1px * var(--outline-scale, 1)) dashed var(--color-accent);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
.element-outline[data-outline='selected'] {
|
|
305
|
+
outline: calc(2px * var(--outline-scale, 1)) solid var(--color-accent);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/* Above its neighbors, so a frame dragged over another stays whole. */
|
|
309
|
+
.frame[data-selected='true'] {
|
|
310
|
+
z-index: 1;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
.frame[data-selected='true'] iframe {
|
|
314
|
+
outline: 2px solid var(--color-accent);
|
|
315
|
+
outline-offset: 2px;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
.frame:focus-visible {
|
|
319
|
+
outline: 2px solid var(--color-accent);
|
|
320
|
+
outline-offset: 6px;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
.box {
|
|
324
|
+
display: grid;
|
|
325
|
+
grid-template-columns: 1fr 1fr;
|
|
326
|
+
gap: var(--space-xs) var(--space-md);
|
|
327
|
+
margin: var(--space-xs) 0 0;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
.box-row {
|
|
331
|
+
display: flex;
|
|
332
|
+
justify-content: space-between;
|
|
333
|
+
gap: var(--space-sm);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
.box dt {
|
|
337
|
+
color: var(--color-muted);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
.box dd {
|
|
341
|
+
margin: 0;
|
|
342
|
+
color: var(--color-text);
|
|
343
|
+
font-variant-numeric: tabular-nums;
|
|
344
|
+
}
|
|
345
|
+
|
|
278
346
|
.toolbar {
|
|
279
347
|
position: absolute;
|
|
280
348
|
right: var(--space-md);
|
|
@@ -317,3 +385,65 @@ code {
|
|
|
317
385
|
border-bottom: 1px solid var(--color-border);
|
|
318
386
|
}
|
|
319
387
|
}
|
|
388
|
+
|
|
389
|
+
.frame-row {
|
|
390
|
+
display: flex;
|
|
391
|
+
align-items: center;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
.frame-row .row {
|
|
395
|
+
flex: 1;
|
|
396
|
+
min-width: 0;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
.disclosure {
|
|
400
|
+
flex: none;
|
|
401
|
+
width: 24px;
|
|
402
|
+
height: 24px;
|
|
403
|
+
padding: 0;
|
|
404
|
+
border: 0;
|
|
405
|
+
border-radius: var(--radius-action);
|
|
406
|
+
background: none;
|
|
407
|
+
color: var(--color-muted);
|
|
408
|
+
font: inherit;
|
|
409
|
+
cursor: pointer;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
.disclosure:hover {
|
|
413
|
+
background: var(--color-surface);
|
|
414
|
+
color: var(--color-text);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
.layers {
|
|
418
|
+
margin: 0;
|
|
419
|
+
padding: 0 0 0 var(--space-sm);
|
|
420
|
+
list-style: none;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
.layers-root {
|
|
424
|
+
padding-left: var(--space-lg);
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
.layer {
|
|
428
|
+
min-height: 24px;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
.row-meta.layer-text {
|
|
432
|
+
flex-shrink: 1;
|
|
433
|
+
min-width: 0;
|
|
434
|
+
overflow: hidden;
|
|
435
|
+
text-overflow: ellipsis;
|
|
436
|
+
white-space: nowrap;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
/* One property a row, since a computed color or font stack outgrows a half. */
|
|
440
|
+
.styles {
|
|
441
|
+
grid-template-columns: 1fr;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
.styles dd {
|
|
445
|
+
min-width: 0;
|
|
446
|
+
overflow: hidden;
|
|
447
|
+
text-overflow: ellipsis;
|
|
448
|
+
white-space: nowrap;
|
|
449
|
+
}
|