2026-01-27 10:57:42 +00:00
|
|
|
|
import {
|
|
|
|
|
|
property,
|
|
|
|
|
|
state,
|
|
|
|
|
|
html,
|
|
|
|
|
|
customElement,
|
|
|
|
|
|
css,
|
|
|
|
|
|
cssManager,
|
|
|
|
|
|
type TemplateResult,
|
|
|
|
|
|
type CSSResult,
|
|
|
|
|
|
} from '@design.estate/dees-element';
|
|
|
|
|
|
import { DeesTileBase } from '../dees-tile-shared/DeesTileBase.js';
|
|
|
|
|
|
import { tileBaseStyles } from '../dees-tile-shared/styles.js';
|
|
|
|
|
|
import { demo } from './demo.js';
|
|
|
|
|
|
|
|
|
|
|
|
declare global {
|
|
|
|
|
|
interface HTMLElementTagNameMap {
|
|
|
|
|
|
'dees-tile-note': DeesTileNote;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@customElement('dees-tile-note')
|
|
|
|
|
|
export class DeesTileNote extends DeesTileBase {
|
|
|
|
|
|
public static demo = demo;
|
|
|
|
|
|
public static demoGroups = ['Media'];
|
|
|
|
|
|
public static styles = [
|
|
|
|
|
|
...tileBaseStyles,
|
|
|
|
|
|
css`
|
|
|
|
|
|
.note-content {
|
|
|
|
|
|
position: relative;
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
height: 100%;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
background: ${cssManager.bdTheme('#ffffff', 'hsl(60 5% 96%)')};
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.note-header {
|
|
|
|
|
|
padding: 12px 14px 8px;
|
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.note-title {
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
font-weight: 700;
|
|
|
|
|
|
color: ${cssManager.bdTheme('hsl(215 20% 20%)', 'hsl(215 20% 20%)')};
|
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
text-overflow: ellipsis;
|
|
|
|
|
|
line-height: 1.3;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.note-body {
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
padding: 0 14px 14px;
|
|
|
|
|
|
position: relative;
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.note-text {
|
|
|
|
|
|
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
|
|
|
|
|
font-size: 10px;
|
|
|
|
|
|
line-height: 1.5;
|
|
|
|
|
|
color: ${cssManager.bdTheme('hsl(215 10% 40%)', 'hsl(215 10% 35%)')};
|
|
|
|
|
|
white-space: pre-wrap;
|
|
|
|
|
|
word-wrap: break-word;
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
margin: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.note-fade {
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
bottom: 0;
|
|
|
|
|
|
left: 0;
|
|
|
|
|
|
right: 0;
|
|
|
|
|
|
height: 60px;
|
|
|
|
|
|
background: linear-gradient(
|
|
|
|
|
|
transparent,
|
|
|
|
|
|
${cssManager.bdTheme('#ffffff', 'hsl(60 5% 96%)')}
|
|
|
|
|
|
);
|
|
|
|
|
|
pointer-events: none;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-27 11:36:24 +00:00
|
|
|
|
.tile-badge-topright.note-language {
|
2026-01-27 10:57:42 +00:00
|
|
|
|
background: ${cssManager.bdTheme('hsl(215 20% 92%)', 'hsl(215 20% 88%)')};
|
|
|
|
|
|
color: ${cssManager.bdTheme('hsl(215 16% 50%)', 'hsl(215 16% 40%)')};
|
|
|
|
|
|
font-size: 9px;
|
|
|
|
|
|
text-transform: uppercase;
|
|
|
|
|
|
z-index: 5;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.note-lines {
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
top: 0;
|
|
|
|
|
|
left: 0;
|
|
|
|
|
|
bottom: 0;
|
|
|
|
|
|
width: 34px;
|
|
|
|
|
|
border-right: 1px solid ${cssManager.bdTheme('hsl(0 70% 85%)', 'hsl(0 50% 80%)')};
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
padding-top: 12px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.line-number {
|
|
|
|
|
|
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
|
|
|
|
|
font-size: 9px;
|
|
|
|
|
|
line-height: 15px; /* matches 10px * 1.5 line-height */
|
|
|
|
|
|
color: ${cssManager.bdTheme('hsl(215 10% 75%)', 'hsl(215 10% 70%)')};
|
|
|
|
|
|
text-align: right;
|
|
|
|
|
|
padding-right: 6px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
`,
|
|
|
|
|
|
] as any;
|
|
|
|
|
|
|
|
|
|
|
|
@property({ type: String })
|
|
|
|
|
|
accessor title: string = '';
|
|
|
|
|
|
|
|
|
|
|
|
@property({ type: String })
|
|
|
|
|
|
accessor content: string = '';
|
|
|
|
|
|
|
|
|
|
|
|
@property({ type: String })
|
|
|
|
|
|
accessor language: string = '';
|
|
|
|
|
|
|
|
|
|
|
|
@state()
|
|
|
|
|
|
accessor isHovering: boolean = false;
|
|
|
|
|
|
|
|
|
|
|
|
private noteBodyElement: HTMLElement | null = null;
|
|
|
|
|
|
|
|
|
|
|
|
protected renderTileContent(): TemplateResult {
|
|
|
|
|
|
const lines = this.content.split('\n');
|
|
|
|
|
|
|
|
|
|
|
|
return html`
|
|
|
|
|
|
<div class="note-content">
|
|
|
|
|
|
${this.language ? html`
|
2026-01-27 11:36:24 +00:00
|
|
|
|
<div class="tile-badge-topright note-language">${this.language}</div>
|
2026-01-27 10:57:42 +00:00
|
|
|
|
` : ''}
|
|
|
|
|
|
|
|
|
|
|
|
${this.title ? html`
|
|
|
|
|
|
<div class="note-header">
|
|
|
|
|
|
<div class="note-title">${this.title}</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
` : ''}
|
|
|
|
|
|
|
|
|
|
|
|
<div class="note-body">
|
|
|
|
|
|
<pre class="note-text">${lines.join('\n')}</pre>
|
|
|
|
|
|
${!this.isHovering ? html`<div class="note-fade"></div>` : ''}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
${this.isHovering && lines.length > 12 ? html`
|
2026-01-27 11:36:24 +00:00
|
|
|
|
<div class="tile-badge-corner">
|
2026-01-27 10:57:42 +00:00
|
|
|
|
Line ${this.getVisibleLineRange(lines.length)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
` : ''}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
${this.clickable ? html`
|
|
|
|
|
|
<div class="tile-overlay">
|
|
|
|
|
|
<dees-icon icon="lucide:FileText"></dees-icon>
|
|
|
|
|
|
<span>Open Note</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
` : ''}
|
|
|
|
|
|
`;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
protected getTileClickDetail(): Record<string, unknown> {
|
|
|
|
|
|
return {
|
|
|
|
|
|
title: this.title,
|
|
|
|
|
|
content: this.content,
|
|
|
|
|
|
language: this.language,
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
protected onTileMouseEnter(): void {
|
|
|
|
|
|
this.isHovering = true;
|
|
|
|
|
|
if (!this.noteBodyElement) {
|
|
|
|
|
|
this.noteBodyElement = this.shadowRoot?.querySelector('.note-body') as HTMLElement;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
protected onTileMouseLeave(): void {
|
|
|
|
|
|
this.isHovering = false;
|
|
|
|
|
|
if (this.noteBodyElement) {
|
|
|
|
|
|
this.noteBodyElement.scrollTop = 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
protected onTileMouseMove(e: MouseEvent): void {
|
|
|
|
|
|
if (!this.isHovering || !this.noteBodyElement) return;
|
|
|
|
|
|
|
|
|
|
|
|
const totalLines = this.content.split('\n').length;
|
|
|
|
|
|
if (totalLines <= 12) return;
|
|
|
|
|
|
|
|
|
|
|
|
const rect = this.getBoundingClientRect();
|
2026-01-27 11:10:39 +00:00
|
|
|
|
const x = e.clientX - rect.left;
|
|
|
|
|
|
const percentage = Math.max(0, Math.min(1, x / rect.width));
|
2026-01-27 10:57:42 +00:00
|
|
|
|
|
|
|
|
|
|
const maxScroll = this.noteBodyElement.scrollHeight - this.noteBodyElement.clientHeight;
|
|
|
|
|
|
this.noteBodyElement.scrollTop = percentage * maxScroll;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private getVisibleLineRange(totalLines: number): string {
|
|
|
|
|
|
if (!this.noteBodyElement) return `1–12 of ${totalLines}`;
|
|
|
|
|
|
const lineHeight = 15; // 10px font × 1.5 line-height
|
|
|
|
|
|
const firstLine = Math.floor(this.noteBodyElement.scrollTop / lineHeight) + 1;
|
|
|
|
|
|
const visibleCount = Math.floor(this.noteBodyElement.clientHeight / lineHeight);
|
|
|
|
|
|
const lastLine = Math.min(firstLine + visibleCount - 1, totalLines);
|
|
|
|
|
|
return `${firstLine}–${lastLine} of ${totalLines}`;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|