Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -40,4 +40,9 @@ testem.log
# System files
.DS_Store
Thumbs.db
.env
.env

# Generated build artifacts
/projects/budgetkey/src/assets/subject-dashboards/index.json

.playwright-mcp
961 changes: 896 additions & 65 deletions package-lock.json

Large diffs are not rendered by default.

8 changes: 5 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@
"scripts": {
"ng": "ng",
"start": "ng serve",
"build": "ng build",
"build-dev": "ng build --configuration staging",
"watch": "ng build --watch --configuration development",
"generate:subject-dashboards-index": "node projects/budgetkey/scripts/generate-subject-dashboards-index.js",
"build": "npm run generate:subject-dashboards-index && ng build",
"build-dev": "npm run generate:subject-dashboards-index && ng build --configuration staging",
"watch": "npm run generate:subject-dashboards-index && ng build --watch --configuration development",
"test": "ng test",
"serve:ssr:budgetkey": "node dist/budgetkey/server/server.mjs"
},
Expand All @@ -33,6 +34,7 @@
"d3-transition": "^3.0.1",
"dayjs": "^1.11.9",
"express": "^4.18.2",
"mermaid": "^11.16.0",
"mushonkey": "^0.3.5",
"rxjs": "~7.8.0",
"showdown": "^2.1.0",
Expand Down
96 changes: 96 additions & 0 deletions projects/budgetkey/scripts/generate-subject-dashboards-index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
const fs = require('fs');
const path = require('path');

const ASSETS_DIR = path.join(__dirname, '..', 'src', 'assets', 'subject-dashboards');
const INDEX_PATH = path.join(ASSETS_DIR, 'index.json');
const REQUIRED_FIELDS = ['title', 'created', 'updated', 'model', 'path'];

function walkMarkdownFiles(dir) {
let results = [];
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
const fullPath = path.join(dir, entry.name);
if (entry.isDirectory()) {
results = results.concat(walkMarkdownFiles(fullPath));
} else if (entry.isFile() && entry.name.endsWith('.md')) {
results.push(fullPath);
} else if (entry.isFile()) {
const slug = path.relative(ASSETS_DIR, fullPath).split(path.sep).join('/');
console.log(`[generate-subject-dashboards-index] Skipping ${slug}: not a markdown file.`);
}
}
return results;
}

function depthOf(filePath) {
return path.relative(ASSETS_DIR, filePath).split(path.sep).length;
}

function parseFrontmatter(content) {
const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
if (!match) {
return null;
}

const frontmatter = {};
for (const line of match[1].split(/\r?\n/)) {
const fieldMatch = line.match(/^([a-zA-Z_]+):\s*(.*)$/);
if (!fieldMatch) {
continue;
}
const [, key, rawValue] = fieldMatch;
frontmatter[key] = rawValue.trim().replace(/^["'](.*)["']$/, '$1');
}
return frontmatter;
}

function toSlug(filePath) {
return path
.relative(ASSETS_DIR, filePath)
.replace(/\.md$/, '')
.split(path.sep)
.join('/');
}

function buildIndex() {
const entries = [];
const markdownFiles = walkMarkdownFiles(ASSETS_DIR);

for (const filePath of markdownFiles) {
const content = fs.readFileSync(filePath, 'utf8');
const frontmatter = parseFrontmatter(content);
const slug = toSlug(filePath);

if (!frontmatter) {
console.warn(`[generate-subject-dashboards-index] Skipping ${slug}: no frontmatter block found.`);
continue;
}

const missingFields = REQUIRED_FIELDS.filter((field) => !frontmatter[field]);
if (missingFields.length > 0) {
console.warn(
`[generate-subject-dashboards-index] Skipping ${slug}: missing required frontmatter field(s): ${missingFields.join(', ')}.`,
);
continue;
}

entries.push({
slug,
depth: depthOf(filePath),
title: frontmatter.title,
created: frontmatter.created,
updated: frontmatter.updated,
model: frontmatter.model,
path: frontmatter.path,
});
}

entries.sort((a, b) => b.depth - a.depth || a.slug.localeCompare(b.slug));
const orderedEntries = entries.map(({ depth, ...entry }) => entry);

fs.writeFileSync(INDEX_PATH, JSON.stringify(orderedEntries, null, 2), 'utf8');
console.log(
`[generate-subject-dashboards-index] Scanned ${markdownFiles.length} markdown file(s), wrote ${orderedEntries.length} entries to ${INDEX_PATH}.`,
);
}

buildIndex();
1 change: 1 addition & 0 deletions projects/budgetkey/src/app/app-routing.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ const routes: Routes = [
{ path: 'p', loadChildren: () => import('./profile/profile.module').then(m => m.ProfileModule) },
{ path: 'l', loadChildren: () => import('./list-page/list-page.module').then(m => m.ListPageModule) },
{ path: 'dashboards', loadChildren: () => import('./dashboards/dashboards.module').then(m => m.DashboardsModule) },
{ path: 'subject-dashboards', loadChildren: () => import('./subject-dashboards/subject-dashboards.module').then(m => m.SubjectDashboardsModule) },
{ path: 'not-found', component: PageNotFoundComponent },
{ path: '**', pathMatch: 'full', component: PageNotFoundComponent },
];
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<app-container [showHeader]="true" [showSearchBar]="true">
<div class="main" role="main">
<ng-container *ngIf="!notFound; else notFoundState">
<ng-container *ngIf="meta">
<header class="dashboard-meta">
<h1>{{ meta.title }}</h1>
<ul class="meta-tags">
<li><span class="meta-label">נוצר</span><span>{{ meta.created }}</span></li>
<li><span class="meta-label">עודכן</span><span>{{ meta.updated }}</span></li>
<li><span class="meta-label">מודל</span><span>{{ meta.model }}</span></li>
<li><span class="meta-label">נתיב</span><span>{{ meta.path }}</span></li>
</ul>
</header>
<div class="md" dir="auto" [innerHtml]="html" #mdContainer (click)="onContentClick($event)"></div>
</ng-container>
</ng-container>
<ng-template #notFoundState>
<p class="not-found">הדף המבוקש לא נמצא.</p>
</ng-template>
</div>
</app-container>
Original file line number Diff line number Diff line change
@@ -0,0 +1,222 @@
@import '../../../common.less';

div.main {
align-items: center;
display: flex;
flex-flow: column;
width: 100%;
padding: 0 10px;
}

div.dashboard-meta {
max-width: 770px;
width: 100%;
padding-top: 40px;

h1 {
color: @color-fill-lead;
.font-miriam;
font-size: 36px;
font-weight: bold;
line-height: 44px;
margin: 0 0 12px 0;
}

ul.meta-tags {
display: flex;
flex-flow: row wrap;
gap: 8px;
list-style: none;
margin: 0 0 24px 0;
padding: 0;

li {
display: flex;
flex-flow: row;
align-items: baseline;
gap: 4px;
background-color: @color-lists-lighter-blue;
border-radius: 14px;
padding: 4px 12px;
.font-abraham;
font-size: 13px;
color: @color-tertiary-700;

span.meta-label {
font-weight: 700;
opacity: 0.75;
}
}
}
}

p.not-found {
color: @color-gray-700;
.font-abraham;
font-size: 20px;
line-height: 26px;
padding-top: 60px;
}

div.md {
max-width: 770px;
width: 100%;
padding-bottom: 60px;
}

::ng-deep {
div.md {
h1 { color: @color-fill-lead; .font-miriam; font-size: 32px; font-weight: bold; line-height: 40px; margin-top: 40px; }
h2 { color: @color-fill-lead; .font-miriam; font-size: 24px; font-weight: 700; line-height: 34px; margin-top: 36px; }
h3 { color: @color-tertiary-700; .font-miriam; font-size: 20px; font-weight: 700; line-height: 28px; margin-top: 28px; }
h4 { color: @color-gray-700; .font-abraham; font-size: 18px; font-weight: 700; line-height: 24px; }

p, li { color: @color-gray-700; .font-abraham; font-size: 17px; line-height: 27px; }

strong { color: @color-gray-800; }

blockquote {
margin: 16px 0;
padding: 4px 16px;
border-inline-start: 3px solid @color-lists-blue2;
color: @color-gray-600;
.font-abraham;
}

a {
color: @color-fill-lead;
text-decoration: underline;
text-underline-offset: 2px;

&:hover { color: darken(@color-fill-lead, 10%); }
}

pre > code {
direction: ltr;
display: block;
background: @color-gray-800;
color: #F7FAFC;
border-radius: 6px;
padding: 12px 16px;
overflow-x: auto;
font-size: 14px;
line-height: 22px;
}

table {
display: block;
overflow-x: auto;
border-collapse: collapse;
width: 100%;
margin: 24px 0;
.font-abraham;
font-size: 15px;
color: @color-gray-700;
border-radius: 6px;
box-shadow: 0 1px 2px 0 rgba(189, 189, 189, 0.6);
}
thead { display: table-header-group; }
tbody { display: table-row-group; }
tr { display: table-row; }
th, td {
display: table-cell;
border: 1px solid @color-gray-100;
padding: 10px 14px;
text-align: start;
white-space: nowrap;
}
th {
background: @color-lists-lighter-blue;
color: @color-tertiary-700;
font-weight: 700;
}
tbody tr:nth-child(even) td {
background: #FAFBFC;
}

details.md-section {
> summary {
display: flex;
align-items: center;
gap: 8px;
cursor: pointer;
list-style: none;
user-select: none;
border-radius: 6px;
margin: 0 -8px;
padding: 2px 8px;
transition: background-color 150ms ease;

&::-webkit-details-marker { display: none; }
&::marker { display: none; }

&::before {
content: '';
flex: 0 0 auto;
width: 7px;
height: 7px;
border-inline-end: 2px solid currentColor;
border-block-end: 2px solid currentColor;
color: @color-fill-lead;
opacity: 0.55;
transform: rotate(45deg);
transition: transform 150ms ease;
}

&:hover { background-color: @color-lists-lighter-blue; }

&:focus-visible {
outline: 2px solid @color-fill-lead;
outline-offset: 2px;
}

h1, h2, h3, h4, h5, h6 { margin: 0; }
}

&[open] > summary::before { transform: rotate(-45deg); }

details.md-section {
margin-inline-start: 2px;
padding-inline-start: 14px;
border-inline-start: 2px solid @color-gray-100;
}
}

div.mermaid-diagram {
display: flex;
justify-content: center;
margin: 28px 0;
overflow-x: auto;
background: @color-white;
border: 1px solid @color-gray-100;
border-radius: 8px;
box-shadow: 0 1px 2px 0 rgba(189, 189, 189, 0.6);
padding: 20px;
// Mermaid's layout math (text-anchor placement, bounding-box sizing)
// assumes an LTR context. The page's `dir="auto"` container resolves
// to rtl for Hebrew content, and that inherits into the diagram's
// <text> elements, flipping which edge text-anchor="start"/"end"
// anchor to — e.g. pie chart legends end up right-anchored and
// growing left into the pie instead of right, away from it. Forcing
// ltr here restores the layout Mermaid actually computed; Hebrew
// runs inside still bidi-reorder correctly for reading.
direction: ltr;

svg { max-width: 100%; }
}

div.plotly-embed {
margin: 28px 0;
overflow-x: auto;
background: @color-white;
border: 1px solid @color-gray-100;
border-radius: 8px;
box-shadow: 0 1px 2px 0 rgba(189, 189, 189, 0.6);
padding: 20px;
// Plotly's own layout (axis ticks, legend, hover labels) isn't
// RTL-aware; force ltr so it renders the way Plotly computed it,
// matching the same fix applied to div.mermaid-diagram above.
direction: ltr;
}
}
}
Loading