一些检查失败了
automerge / automerge (push) Has been cancelled
react-native-android-build-apk / AssembleRelease (push) Has been cancelled
Build macOS M1 / Main (push) Has been cancelled
Joplin Continuous Integration / Main (macos-15-intel) (push) Has been cancelled
Joplin Continuous Integration / Main (ubuntu-22.04) (push) Has been cancelled
Joplin Continuous Integration / Main (ubuntu-22.04-arm) (push) Has been cancelled
Joplin Continuous Integration / Main (windows-11-vs2026-arm) (push) Has been cancelled
Joplin Continuous Integration / Main (windows-2025) (push) Has been cancelled
Joplin Continuous Integration / ServerDockerImage (ubuntu-22.04) (push) Has been cancelled
Joplin Continuous Integration / ServerDockerImage (ubuntu-22.04-arm) (push) Has been cancelled
Joplin UI tests / Main (ubuntu-22.04) (push) Has been cancelled
Joplin UI tests / Main (windows-2025) (push) Has been cancelled
Joplin UI tests / Mobile (push) Has been cancelled
Close stale issues / ProcessStaleIssues (push) Has been cancelled
498 行
13 KiB
SCSS
498 行
13 KiB
SCSS
$selection-color: #4a90e2;
|
|
$selection-shadow: rgba(74, 144, 226, 0.25);
|
|
$card-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
|
|
$card-shadow-selected: 0 4px 12px $selection-shadow;
|
|
$active-bg: rgba(74, 144, 226, 0.18);
|
|
$active-fg: #2766b8;
|
|
$hover-bg: rgba(127, 127, 127, 0.12);
|
|
|
|
.whiteboard-editor {
|
|
display: flex;
|
|
flex-direction: column;
|
|
flex: 1;
|
|
min-height: 0;
|
|
|
|
> .error {
|
|
display: flex;
|
|
flex-direction: column;
|
|
flex: 1;
|
|
min-height: 0;
|
|
padding: 24px;
|
|
overflow: auto;
|
|
|
|
> .panel {
|
|
background-color: var(--joplin-warning-background-color);
|
|
color: var(--joplin-color);
|
|
padding: 16px;
|
|
border-radius: 4px;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 12px;
|
|
|
|
> .title { font-weight: 600; }
|
|
> .detail { white-space: pre-wrap; font-family: monospace; font-size: 12px; }
|
|
}
|
|
}
|
|
}
|
|
|
|
.whiteboard-surface {
|
|
position: relative;
|
|
flex: 1;
|
|
width: 100%;
|
|
height: 100%;
|
|
background: var(--joplin-background-color3);
|
|
outline: none;
|
|
|
|
.react-flow {
|
|
--xy-background-color-default: var(--joplin-background-color3);
|
|
--xy-background-pattern-dots-color-default: var(--joplin-divider-color);
|
|
--xy-edge-stroke-default: var(--joplin-divider-color);
|
|
--xy-edge-stroke-selected-default: #{$selection-color};
|
|
--xy-connectionline-stroke-default: var(--joplin-color-faded);
|
|
--xy-attribution-background-color-default: var(--joplin-background-color);
|
|
--xy-minimap-background-color-default: var(--joplin-background-color);
|
|
--xy-minimap-mask-background-color-default: var(--joplin-background-color3);
|
|
--xy-minimap-node-background-color-default: var(--joplin-color-faded);
|
|
--xy-node-color-default: var(--joplin-color);
|
|
--xy-node-background-color-default: var(--joplin-background-color);
|
|
--xy-controls-button-background-color-default: var(--joplin-background-color);
|
|
--xy-controls-button-color-default: var(--joplin-color);
|
|
--xy-controls-button-border-color-default: var(--joplin-divider-color);
|
|
}
|
|
|
|
// Only bump the stroke width on selection — the stroke colour is set
|
|
// inline (edge's own colour, or the default blue when unset) so we don't
|
|
// override it here.
|
|
.react-flow__edge.selected .react-flow__edge-path,
|
|
.react-flow__edge:focus .react-flow__edge-path,
|
|
.react-flow__edge:focus-visible .react-flow__edge-path {
|
|
stroke-width: 2 !important;
|
|
}
|
|
.react-flow__edge.selected .react-flow__edge-textbg { fill: $selection-color; }
|
|
.react-flow__edge.selected .react-flow__edge-text { fill: #ffffff; }
|
|
|
|
.react-flow__node .react-flow__handle {
|
|
opacity: 0;
|
|
transition: opacity 120ms ease;
|
|
background: var(--joplin-color-faded);
|
|
}
|
|
.react-flow__node:hover .react-flow__handle,
|
|
.react-flow__node.selected .react-flow__handle,
|
|
.react-flow__handle.connectingfrom,
|
|
.react-flow__handle.connectingto { opacity: 1; }
|
|
|
|
.react-flow__minimap { width: 160px; height: 100px; }
|
|
}
|
|
|
|
.whiteboard-action-panel {
|
|
position: absolute;
|
|
z-index: 10;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0;
|
|
padding: 4px;
|
|
background: var(--joplin-background-color);
|
|
border: 1px solid var(--joplin-divider-color);
|
|
border-radius: 8px;
|
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.10);
|
|
font-size: 12px;
|
|
height: 36px;
|
|
color: var(--joplin-color);
|
|
user-select: none;
|
|
|
|
> .input { user-select: text; }
|
|
|
|
&.-bottom-center { bottom: 16px; left: 50%; transform: translateX(-50%); }
|
|
&.-top-center { top: 16px; left: 50%; transform: translateX(-50%); }
|
|
&.-top-right { top: 8px; right: 8px; }
|
|
|
|
> .caption {
|
|
color: var(--joplin-color-faded);
|
|
padding: 0 10px;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
> .divider {
|
|
width: 1px;
|
|
height: 20px;
|
|
background: var(--joplin-divider-color);
|
|
margin: 0 4px;
|
|
}
|
|
|
|
> .button {
|
|
height: 28px;
|
|
padding: 0 10px;
|
|
font-size: 12px;
|
|
border: none;
|
|
background: transparent;
|
|
color: var(--joplin-color);
|
|
cursor: pointer;
|
|
border-radius: 6px;
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 4px;
|
|
white-space: nowrap;
|
|
user-select: none;
|
|
|
|
&:hover:not(:disabled):not(.-active) { background: $hover-bg; }
|
|
&.-active { background: $active-bg; color: $active-fg; }
|
|
&:disabled { opacity: 0.45; cursor: default; }
|
|
}
|
|
|
|
> .input {
|
|
height: 24px;
|
|
padding: 0 8px;
|
|
font-size: 12px;
|
|
border: 1px solid var(--joplin-divider-color);
|
|
border-radius: 4px;
|
|
margin: 0 4px;
|
|
background: var(--joplin-background-color);
|
|
color: var(--joplin-color);
|
|
}
|
|
|
|
> .swatch {
|
|
width: 18px;
|
|
height: 18px;
|
|
border-radius: 50%;
|
|
border: 1px solid var(--joplin-divider-color);
|
|
padding: 0;
|
|
margin: 0 2px;
|
|
cursor: pointer;
|
|
background: transparent;
|
|
|
|
&:hover { transform: scale(1.15); }
|
|
&.-active { outline: 2px solid $selection-color; outline-offset: 1px; }
|
|
|
|
// "No colour" swatch: diagonal slash on empty background.
|
|
&.-none {
|
|
position: relative;
|
|
background: var(--joplin-background-color);
|
|
}
|
|
&.-none::after {
|
|
content: '';
|
|
position: absolute;
|
|
top: 50%;
|
|
left: -1px;
|
|
right: -1px;
|
|
height: 1px;
|
|
background: var(--joplin-color-faded);
|
|
transform: rotate(-45deg);
|
|
}
|
|
}
|
|
}
|
|
|
|
.whiteboard-node {
|
|
width: 100%;
|
|
height: 100%;
|
|
border: 1px solid var(--joplin-divider-color);
|
|
border-radius: 6px;
|
|
background: var(--joplin-background-color);
|
|
overflow: hidden;
|
|
box-shadow: $card-shadow;
|
|
box-sizing: border-box;
|
|
display: flex;
|
|
flex-direction: column;
|
|
color: var(--joplin-color);
|
|
|
|
// Only bump the border width and shadow on selection — the border colour
|
|
// is either the node's own colour (inline style) or falls back to the
|
|
// default selection blue set inline by the renderer.
|
|
&.-selected {
|
|
border-width: 2px;
|
|
border-style: solid;
|
|
box-shadow: $card-shadow-selected;
|
|
}
|
|
|
|
&.-text { overflow: auto; padding: 8px; font-size: 13px; line-height: 1.4; }
|
|
|
|
> .header {
|
|
font-size: 11px;
|
|
color: var(--joplin-color-faded);
|
|
padding: 4px 8px;
|
|
border-bottom: 1px solid var(--joplin-divider-color);
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.5px;
|
|
flex-shrink: 0;
|
|
|
|
&.-note-title {
|
|
font-size: 12px;
|
|
font-weight: 600;
|
|
color: var(--joplin-color);
|
|
padding: 5px 8px;
|
|
text-transform: none;
|
|
letter-spacing: 0;
|
|
white-space: nowrap;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
}
|
|
}
|
|
|
|
> .body {
|
|
flex: 1;
|
|
padding: 8px;
|
|
overflow: auto;
|
|
word-break: break-word;
|
|
font-size: 13px;
|
|
line-height: 1.4;
|
|
color: var(--joplin-color);
|
|
|
|
&.-deleted, &.-locked { color: var(--joplin-color-faded); font-style: italic; }
|
|
&.-url { word-break: break-all; }
|
|
}
|
|
|
|
> .image {
|
|
max-width: 100%;
|
|
max-height: 100%;
|
|
object-fit: contain;
|
|
align-self: center;
|
|
flex: 1;
|
|
}
|
|
|
|
> .editor {
|
|
width: 100%;
|
|
height: 100%;
|
|
border: none;
|
|
outline: none;
|
|
resize: none;
|
|
font-family: inherit;
|
|
font-size: 13px;
|
|
line-height: 1.4;
|
|
color: var(--joplin-color);
|
|
background: transparent;
|
|
}
|
|
|
|
> .empty { color: var(--joplin-color-faded); }
|
|
}
|
|
|
|
// Groups are visual containers. pointer-events:none is set inline on the
|
|
// React Flow wrapper via canvasNodeToFlowNode (CSS classes can't override
|
|
// React Flow's own inline style on the wrapper). The label and handle below
|
|
// re-enable pointer events so they catch select / drag / rename.
|
|
.whiteboard-group {
|
|
position: relative;
|
|
width: 100%;
|
|
height: 100%;
|
|
|
|
// Two-layer construction: .background is the rounded fill (no pointer
|
|
// events). .ring is the dashed border with no background — because
|
|
// pointer-events:auto on an unpainted element only catches the painted
|
|
// pixels, the interior of .ring doesn't catch clicks. Only the border
|
|
// line does, which is exactly what we want: drag the line to move the
|
|
// group, click anywhere else to pan.
|
|
> .background {
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
right: 0;
|
|
bottom: 0;
|
|
border-radius: 8px;
|
|
background: rgba(127, 127, 127, 0.06);
|
|
pointer-events: none;
|
|
}
|
|
|
|
// Four edge strips form the dashed border. Each strip is a separate
|
|
// painted element with pointer-events:auto, so clicks on the border line
|
|
// catch drags. The interior has no overlay at all, so clicks there fall
|
|
// through to the pane (pan / deselect) or to a card on top.
|
|
$edge-thickness: 8px;
|
|
> .edge {
|
|
position: absolute;
|
|
pointer-events: auto;
|
|
cursor: grab;
|
|
|
|
&:active { cursor: grabbing; }
|
|
|
|
&.-top {
|
|
top: -2px;
|
|
left: 0;
|
|
right: 0;
|
|
height: $edge-thickness;
|
|
border-top: 2px dashed var(--joplin-divider-color);
|
|
}
|
|
&.-bottom {
|
|
bottom: -2px;
|
|
left: 0;
|
|
right: 0;
|
|
height: $edge-thickness;
|
|
border-bottom: 2px dashed var(--joplin-divider-color);
|
|
}
|
|
&.-left {
|
|
top: 0;
|
|
bottom: 0;
|
|
left: -2px;
|
|
width: $edge-thickness;
|
|
border-left: 2px dashed var(--joplin-divider-color);
|
|
}
|
|
&.-right {
|
|
top: 0;
|
|
bottom: 0;
|
|
right: -2px;
|
|
width: $edge-thickness;
|
|
border-right: 2px dashed var(--joplin-divider-color);
|
|
}
|
|
}
|
|
|
|
&.-selected > .edge.-top { border-top-color: $selection-color; }
|
|
&.-selected > .edge.-bottom { border-bottom-color: $selection-color; }
|
|
&.-selected > .edge.-left { border-left-color: $selection-color; }
|
|
&.-selected > .edge.-right { border-right-color: $selection-color; }
|
|
|
|
> .anchor {
|
|
position: absolute;
|
|
top: -30px;
|
|
left: -30px;
|
|
width: 24px;
|
|
height: 24px;
|
|
border-radius: 4px;
|
|
background: var(--joplin-background-color);
|
|
border: 1px solid var(--joplin-divider-color);
|
|
color: var(--joplin-color-faded);
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-size: 12px;
|
|
pointer-events: auto;
|
|
cursor: grab;
|
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.10);
|
|
|
|
&:hover { background: $selection-color; color: #fff; border-color: $selection-color; }
|
|
&:active { cursor: grabbing; }
|
|
}
|
|
|
|
&.-selected > .anchor { border-color: $selection-color; color: $selection-color; }
|
|
&.-selected > .anchor:hover { color: #fff; }
|
|
|
|
> .label {
|
|
position: absolute;
|
|
top: 6px;
|
|
left: 10px;
|
|
max-width: calc(100% - 20px);
|
|
font-size: 12px;
|
|
font-weight: 600;
|
|
color: var(--joplin-color-faded);
|
|
white-space: nowrap;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
pointer-events: auto;
|
|
cursor: grab;
|
|
padding: 1px 4px;
|
|
border-radius: 3px;
|
|
|
|
&:hover { background: rgba(127, 127, 127, 0.10); }
|
|
&:active { cursor: grabbing; }
|
|
|
|
> .placeholder { font-weight: 400; font-style: italic; }
|
|
}
|
|
|
|
> input.label {
|
|
border: 1px solid var(--joplin-divider-color);
|
|
border-radius: 4px;
|
|
padding: 1px 6px;
|
|
background: var(--joplin-background-color);
|
|
color: var(--joplin-color);
|
|
font-weight: 600;
|
|
outline: none;
|
|
pointer-events: auto;
|
|
cursor: text;
|
|
}
|
|
}
|
|
|
|
// Re-enable pointer events on the resize handles when a group is selected,
|
|
// since the wrapper itself is pointer-transparent.
|
|
.react-flow__node-wbGroup.selected .react-flow__resize-control {
|
|
pointer-events: auto;
|
|
}
|
|
|
|
.whiteboard-node-promote {
|
|
position: absolute;
|
|
top: -10px;
|
|
right: -10px;
|
|
padding: 2px 8px;
|
|
font-size: 11px;
|
|
border: 1px solid $selection-color;
|
|
border-radius: 10px;
|
|
background: var(--joplin-background-color);
|
|
color: $selection-color;
|
|
cursor: pointer;
|
|
z-index: 5;
|
|
}
|
|
|
|
.wb-card-md {
|
|
flex: 1;
|
|
overflow: auto;
|
|
word-break: break-word;
|
|
font-size: 13px;
|
|
line-height: 1.4;
|
|
|
|
h1 { font-size: 16px; margin: 4px 0; }
|
|
h2 { font-size: 15px; margin: 4px 0; }
|
|
h3 { font-size: 14px; margin: 3px 0; }
|
|
h4, h5, h6 { font-size: 13px; margin: 3px 0; }
|
|
p { margin: 8px 0; }
|
|
// Drop the top margin on whatever comes first so a leading heading /
|
|
// paragraph / list sits flush against the card's own 8px padding
|
|
// instead of stacking margins on top of it. The renderer wraps its
|
|
// output in a `#rendered-md` div, so we also target the first child
|
|
// inside that wrapper — `:first-child` alone would only match the div.
|
|
> :first-child, > * > :first-child { margin-top: 0; }
|
|
ul, ol { margin: 4px 0; padding-left: 20px; }
|
|
li { margin: 2px 0; }
|
|
li.md-checkbox, li.joplin-checkbox { list-style: none; margin-left: -20px; }
|
|
li.md-checkbox input[type=checkbox] { margin-right: 6px; vertical-align: middle; }
|
|
.checkbox-wrapper { display: inline; }
|
|
pre {
|
|
margin: 4px 0;
|
|
padding: 6px;
|
|
font-size: 12px;
|
|
background: var(--joplin-code-background-color);
|
|
border: 1px solid var(--joplin-code-border-color);
|
|
border-radius: 4px;
|
|
overflow: auto;
|
|
color: var(--joplin-code-color);
|
|
}
|
|
code {
|
|
font-size: 12px;
|
|
background: var(--joplin-code-background-color);
|
|
color: var(--joplin-code-color);
|
|
border: 1px solid var(--joplin-code-border-color);
|
|
padding: 0 3px;
|
|
border-radius: 3px;
|
|
}
|
|
pre code { background: transparent; padding: 0; border: none; }
|
|
blockquote {
|
|
margin: 4px 0;
|
|
padding-left: 8px;
|
|
border-left: 3px solid var(--joplin-code-border-color);
|
|
color: var(--joplin-color-faded);
|
|
}
|
|
img { max-width: 100%; height: auto; }
|
|
table {
|
|
font-size: 12px;
|
|
border-collapse: collapse;
|
|
border: 1px solid var(--joplin-code-border-color);
|
|
}
|
|
th, td { padding: 2px 6px; border: 1px solid var(--joplin-code-border-color); }
|
|
th {
|
|
background: var(--joplin-background-color-hover3, var(--joplin-background-color3));
|
|
border-bottom-width: 2px;
|
|
}
|
|
// Only zebra rows for tables with an explicit thead — matches the viewer
|
|
// (see noteStyle.ts) so freeform key/value tables don't get striped.
|
|
table:has(thead) tr:nth-child(even) {
|
|
background: var(--joplin-background-color-hover3, var(--joplin-background-color3));
|
|
}
|
|
hr { margin: 6px 0; border: none; border-top: 2px solid var(--joplin-divider-color); }
|
|
a {
|
|
color: var(--joplin-url-color, #{$selection-color});
|
|
&:hover { text-decoration: underline; }
|
|
}
|
|
mark {
|
|
background: var(--joplin-search-marker-background-color);
|
|
color: var(--joplin-search-marker-color);
|
|
}
|
|
// Match the viewer's checked-checkbox affordance so completed items visually
|
|
// recede without disappearing.
|
|
.md-checkbox input[type=checkbox]:checked { opacity: 0.7; }
|
|
.md-checkbox .checkbox-label-checked { opacity: 0.5; }
|
|
}
|