feat(electron): added opat parsing
This commit is contained in:
@@ -18,7 +18,11 @@
|
||||
</div>
|
||||
</div>
|
||||
<nav class="category-nav">
|
||||
<div class="category-item active" data-category="libplugin" title="libplugin">
|
||||
<div class="category-item" data-category="home" title="Home">
|
||||
<div class="category-icon" style="background-color: #6366f1;">🏠</div>
|
||||
<span class="category-label">Home</span>
|
||||
</div>
|
||||
<div class="category-item" data-category="libplugin" title="libplugin">
|
||||
<div class="category-icon" style="background-color: #3b82f6;">LP</div>
|
||||
<span class="category-label">libplugin</span>
|
||||
</div>
|
||||
@@ -67,14 +71,15 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- OPAT Core content (empty for now) -->
|
||||
<!-- OPAT Core content -->
|
||||
<div class="sidebar-content hidden" data-category="opat">
|
||||
<div class="sidebar-header">
|
||||
<h3>OPAT Core</h3>
|
||||
</div>
|
||||
<div class="empty-state">
|
||||
<p>OPAT tools coming soon...</p>
|
||||
</div>
|
||||
<nav class="sidebar-nav">
|
||||
<input type="file" id="opat-file-input" accept=".opat" class="opat-file-input" style="display: none;"/>
|
||||
<button id="opat-browse-btn" class="nav-button active">Open OPAT File</button>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<!-- SERiF Libraries content (empty for now) -->
|
||||
@@ -89,9 +94,254 @@
|
||||
</aside>
|
||||
|
||||
<main class="content-area">
|
||||
<div id="welcome-screen">
|
||||
<h1>Welcome to 4DSTAR Bundle Manager</h1>
|
||||
<p>Open or create a bundle to get started.</p>
|
||||
<!-- Main Home Screen -->
|
||||
<div id="welcome-screen" class="home-screen">
|
||||
<div class="welcome-hero">
|
||||
<div class="welcome-logo">
|
||||
<div class="star-icon-large">✦</div>
|
||||
<h1 class="welcome-title">4DSTAR</h1>
|
||||
<p class="welcome-subtitle">Stellar Evolution Analysis Suite</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="welcome-content">
|
||||
<div class="welcome-section">
|
||||
<h2>Getting Started</h2>
|
||||
<p>Choose a tool from the sidebar to begin your analysis:</p>
|
||||
|
||||
<div class="feature-grid">
|
||||
<div class="feature-card">
|
||||
<div class="feature-icon" style="background-color: #3b82f6;">LP</div>
|
||||
<div class="feature-info">
|
||||
<h3>libplugin Bundle Manager</h3>
|
||||
<p>Create, open, and manage stellar evolution bundles. Build models and analyze results.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="feature-card">
|
||||
<div class="feature-icon" style="background-color: #f59e0b;">OC</div>
|
||||
<div class="feature-info">
|
||||
<h3>OPAT File Inspector</h3>
|
||||
<p>Inspect and analyze OPAT (Opacity Table) files. View table data and explore index vectors.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="feature-card">
|
||||
<div class="feature-icon" style="background-color: #10b981;">LC</div>
|
||||
<div class="feature-info">
|
||||
<h3>libconstants</h3>
|
||||
<p>Access and manage physical constants and parameters for stellar evolution calculations.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="feature-card">
|
||||
<div class="feature-icon" style="background-color: #ef4444;">SL</div>
|
||||
<div class="feature-info">
|
||||
<h3>SERiF Libraries</h3>
|
||||
<p>Stellar Evolution Rapid interpolation Framework tools and utilities.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="welcome-tips">
|
||||
<h3>Quick Tips</h3>
|
||||
<ul>
|
||||
<li>Use the sidebar navigation to switch between different tools</li>
|
||||
<li>Each tool has its own dedicated workspace and features</li>
|
||||
<li>File operations and settings are accessible from the top menu bar</li>
|
||||
<li>Hover over sidebar icons for quick tool descriptions</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- libplugin Home Screen -->
|
||||
<div id="libplugin-home" class="home-screen hidden">
|
||||
<div class="category-hero">
|
||||
<div class="category-hero-content">
|
||||
<div class="category-icon-large" style="background-color: #3b82f6;">LP</div>
|
||||
<h1 class="category-title">libplugin Bundle Manager</h1>
|
||||
<p class="category-subtitle">Create, manage, and analyze stellar evolution bundles</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="category-content">
|
||||
<div class="feature-section">
|
||||
<h2>Bundle Management</h2>
|
||||
<div class="action-cards">
|
||||
<div class="action-card">
|
||||
<h3>📁 Open Bundle</h3>
|
||||
<p>Load an existing stellar evolution bundle for analysis and modification.</p>
|
||||
</div>
|
||||
<div class="action-card">
|
||||
<h3>➕ Create Bundle</h3>
|
||||
<p>Start a new stellar evolution project with custom parameters and configurations.</p>
|
||||
</div>
|
||||
<div class="action-card">
|
||||
<h3>🔧 Build Models</h3>
|
||||
<p>Compile and execute stellar evolution calculations with your bundle settings.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="info-section">
|
||||
<h3>About libplugin</h3>
|
||||
<p>The libplugin Bundle Manager provides a comprehensive interface for managing stellar evolution projects. Create bundles with specific initial conditions, track evolution parameters, and analyze results through integrated tools.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- OPAT Home Screen -->
|
||||
<div id="opat-home" class="home-screen hidden">
|
||||
<div class="category-hero">
|
||||
<div class="category-hero-content">
|
||||
<div class="category-icon-large" style="background-color: #f59e0b;">OC</div>
|
||||
<h1 class="category-title">OPAT File Inspector</h1>
|
||||
<p class="category-subtitle">Analyze opacity tables and stellar atmosphere data</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="category-content">
|
||||
<div class="feature-section">
|
||||
<h2>OPAT Analysis Tools</h2>
|
||||
<div class="action-cards">
|
||||
<div class="action-card">
|
||||
<h3>📊 Load OPAT File</h3>
|
||||
<p>Import and parse OPAT (Opacity Table) files for detailed inspection.</p>
|
||||
</div>
|
||||
<div class="action-card">
|
||||
<h3>🔍 Explore Data</h3>
|
||||
<p>Navigate through index vectors and examine table structures and metadata.</p>
|
||||
</div>
|
||||
<div class="action-card">
|
||||
<h3>📈 View Tables</h3>
|
||||
<p>Display opacity data in interactive tables with full or filtered views.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="info-section">
|
||||
<h3>About OPAT Files</h3>
|
||||
<p>OPAT (Opacity Table) files contain crucial stellar atmosphere data including opacity coefficients, temperature and density grids, and related thermodynamic properties used in stellar evolution calculations.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- libconstants Home Screen -->
|
||||
<div id="libconstants-home" class="home-screen hidden">
|
||||
<div class="category-hero">
|
||||
<div class="category-hero-content">
|
||||
<div class="category-icon-large" style="background-color: #10b981;">LC</div>
|
||||
<h1 class="category-title">libconstants</h1>
|
||||
<p class="category-subtitle">Physical constants and stellar parameters</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="category-content">
|
||||
<div class="feature-section">
|
||||
<h2>Constants Management</h2>
|
||||
<div class="action-cards">
|
||||
<div class="action-card">
|
||||
<h3>🔢 Physical Constants</h3>
|
||||
<p>Access fundamental physical constants used in stellar evolution calculations.</p>
|
||||
</div>
|
||||
<div class="action-card">
|
||||
<h3>⭐ Stellar Parameters</h3>
|
||||
<p>Manage stellar-specific constants and conversion factors.</p>
|
||||
</div>
|
||||
<div class="action-card">
|
||||
<h3>⚙️ Configuration</h3>
|
||||
<p>Customize constant values and units for specific research needs.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="info-section">
|
||||
<h3>About libconstants</h3>
|
||||
<p>The libconstants module provides a centralized repository of physical constants, conversion factors, and stellar parameters essential for accurate stellar evolution modeling and analysis.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- SERiF Home Screen -->
|
||||
<div id="serif-home" class="home-screen hidden">
|
||||
<div class="category-hero">
|
||||
<div class="category-hero-content">
|
||||
<div class="category-icon-large" style="background-color: #ef4444;">SL</div>
|
||||
<h1 class="category-title">SERiF Libraries</h1>
|
||||
<p class="category-subtitle">Stellar Evolution Rapid interpolation Framework</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="category-content">
|
||||
<div class="feature-section">
|
||||
<h2>SERiF Tools</h2>
|
||||
<div class="action-cards">
|
||||
<div class="action-card">
|
||||
<h3>🚀 Rapid Interpolation</h3>
|
||||
<p>High-performance interpolation algorithms for stellar evolution tracks.</p>
|
||||
</div>
|
||||
<div class="action-card">
|
||||
<h3>📚 Model Libraries</h3>
|
||||
<p>Access pre-computed stellar evolution models and isochrones.</p>
|
||||
</div>
|
||||
<div class="action-card">
|
||||
<h3>🔬 Analysis Framework</h3>
|
||||
<p>Advanced tools for stellar population synthesis and comparison studies.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="info-section">
|
||||
<h3>About SERiF</h3>
|
||||
<p>The Stellar Evolution Rapid interpolation Framework (SERiF) provides optimized tools for working with large stellar evolution datasets, enabling fast interpolation and analysis of stellar models.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="opat-view" class="hidden">
|
||||
<header class="content-header">
|
||||
<h2 id="opat-title">OPAT File Inspector</h2>
|
||||
<div class="action-buttons">
|
||||
<button id="opat-close-btn">Close File</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="tab-nav">
|
||||
<button class="tab-link active" data-tab="opat-overview-tab">File Information</button>
|
||||
<button class="tab-link" data-tab="opat-explorer-tab">Data Explorer</button>
|
||||
</div>
|
||||
|
||||
<div id="opat-tab-content">
|
||||
<div id="opat-overview-tab" class="tab-pane active">
|
||||
<div id="opat-header-info" class="opat-info-content"></div>
|
||||
<div class="opat-info-section">
|
||||
<h4 class="opat-section-title">All Table Tags</h4>
|
||||
<ul id="opat-all-tags-list" class="opat-tags-list"></ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="opat-explorer-tab" class="tab-pane hidden">
|
||||
<div class="opat-info-section">
|
||||
<h4 class="opat-section-title">Data Card Explorer</h4>
|
||||
<div class="opat-selector-group">
|
||||
<label for="opat-index-selector" class="opat-label">Select Index Vector:</label>
|
||||
<select id="opat-index-selector" class="opat-select">
|
||||
<option value="">-- Select an Index Vector --</option>
|
||||
</select>
|
||||
</div>
|
||||
<div id="opat-tables-display" class="opat-tables-display"></div>
|
||||
</div>
|
||||
|
||||
<div class="opat-info-section">
|
||||
<h4 class="opat-section-title">Table Viewer</h4>
|
||||
<div id="opat-table-data-content" class="opat-table-viewer">
|
||||
<p class="opat-placeholder">Click on a table from the 'Data Card Explorer' above to view its data here.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="bundle-view" class="hidden">
|
||||
@@ -309,6 +559,7 @@
|
||||
|
||||
<div id="spinner" class="spinner hidden"></div>
|
||||
|
||||
<script src="opatParser.js"></script>
|
||||
<script src="renderer.js"></script>
|
||||
</body>
|
||||
|
||||
|
||||
375
electron/opatParser.js
Normal file
375
electron/opatParser.js
Normal file
@@ -0,0 +1,375 @@
|
||||
/**
|
||||
* A helper class to read binary data from an ArrayBuffer.
|
||||
* It keeps track of the current position and handles reading different data types.
|
||||
*/
|
||||
class DataReader {
|
||||
/**
|
||||
* @param {ArrayBuffer} arrayBuffer The file content.
|
||||
*/
|
||||
constructor(arrayBuffer) {
|
||||
this.dataView = new DataView(arrayBuffer);
|
||||
this.decoder = new TextDecoder();
|
||||
this.offset = 0;
|
||||
}
|
||||
|
||||
seek(newOffset) {
|
||||
this.offset = newOffset;
|
||||
}
|
||||
|
||||
readString(length) {
|
||||
const strBytes = new Uint8Array(this.dataView.buffer, this.offset, length);
|
||||
this.offset += length;
|
||||
// Trim null characters from the end
|
||||
const firstNull = strBytes.indexOf(0);
|
||||
const nonNullSlice = firstNull === -1 ? strBytes : strBytes.slice(0, firstNull);
|
||||
return this.decoder.decode(nonNullSlice);
|
||||
}
|
||||
|
||||
readBytes(length) {
|
||||
const bytes = new Uint8Array(this.dataView.buffer, this.offset, length);
|
||||
this.offset += length;
|
||||
return bytes;
|
||||
}
|
||||
|
||||
readUint8() {
|
||||
const value = this.dataView.getUint8(this.offset, true);
|
||||
this.offset += 1;
|
||||
return value;
|
||||
}
|
||||
|
||||
readUint16() {
|
||||
const value = this.dataView.getUint16(this.offset, true);
|
||||
this.offset += 2;
|
||||
return value;
|
||||
}
|
||||
|
||||
readUint32() {
|
||||
const value = this.dataView.getUint32(this.offset, true);
|
||||
this.offset += 4;
|
||||
return value;
|
||||
}
|
||||
|
||||
readUint64() {
|
||||
const value = this.dataView.getBigUint64(this.offset, true);
|
||||
this.offset += 8;
|
||||
return value;
|
||||
}
|
||||
|
||||
readFloat64() {
|
||||
const value = this.dataView.getFloat64(this.offset, true);
|
||||
this.offset += 8;
|
||||
return value;
|
||||
}
|
||||
|
||||
readFloat64Array(length) {
|
||||
const byteLength = length * 8;
|
||||
const values = new Float64Array(this.dataView.buffer, this.offset, length);
|
||||
this.offset += byteLength;
|
||||
return values;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Corresponds to the Header struct in opatIO.h
|
||||
*/
|
||||
class Header {
|
||||
constructor() {
|
||||
this.magic = "";
|
||||
this.version = 0;
|
||||
this.numTables = 0;
|
||||
this.headerSize = 0;
|
||||
this.indexOffset = 0n; // BigInt
|
||||
this.creationDate = "";
|
||||
this.sourceInfo = "";
|
||||
this.comment = "";
|
||||
this.numIndex = 0;
|
||||
this.hashPrecision = 0;
|
||||
this.reserved = null;
|
||||
}
|
||||
|
||||
static fromReader(reader) {
|
||||
const h = new Header();
|
||||
h.magic = reader.readString(4);
|
||||
h.version = reader.readUint16();
|
||||
h.numTables = reader.readUint32();
|
||||
h.headerSize = reader.readUint32();
|
||||
h.indexOffset = reader.readUint64();
|
||||
h.creationDate = reader.readString(16);
|
||||
h.sourceInfo = reader.readString(64);
|
||||
h.comment = reader.readString(128);
|
||||
h.numIndex = reader.readUint16();
|
||||
h.hashPrecision = reader.readUint8();
|
||||
h.reserved = reader.readBytes(23);
|
||||
return h;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Corresponds to the CardHeader struct in opatIO.h
|
||||
*/
|
||||
class CardHeader {
|
||||
constructor() {
|
||||
this.magic = "";
|
||||
this.numTables = 0;
|
||||
this.headerSize = 0;
|
||||
this.indexOffset = 0n;
|
||||
this.cardSize = 0n;
|
||||
this.comment = "";
|
||||
this.reserved = null;
|
||||
}
|
||||
|
||||
static fromReader(reader) {
|
||||
const h = new CardHeader();
|
||||
h.magic = reader.readString(4);
|
||||
h.numTables = reader.readUint32();
|
||||
h.headerSize = reader.readUint32();
|
||||
h.indexOffset = reader.readUint64();
|
||||
h.cardSize = reader.readUint64();
|
||||
h.comment = reader.readString(128);
|
||||
h.reserved = reader.readBytes(100);
|
||||
return h;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Corresponds to the CardCatalogEntry struct in opatIO.h
|
||||
*/
|
||||
class CardCatalogEntry {
|
||||
constructor() {
|
||||
this.index = []; // This will be an array of numbers (FloatIndexVector)
|
||||
this.byteStart = 0n;
|
||||
this.byteEnd = 0n;
|
||||
this.sha256 = null; // Uint8Array
|
||||
}
|
||||
|
||||
/**
|
||||
* In JS, we can use a stringified version of the index vector as a key for Maps.
|
||||
*/
|
||||
getKey() {
|
||||
return JSON.stringify(this.index);
|
||||
}
|
||||
|
||||
static fromReader(reader, numIndex) {
|
||||
const entry = new CardCatalogEntry();
|
||||
entry.index = Array.from(reader.readFloat64Array(numIndex));
|
||||
entry.byteStart = reader.readUint64();
|
||||
entry.byteEnd = reader.readUint64();
|
||||
entry.sha256 = reader.readBytes(32);
|
||||
return entry;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Corresponds to the TableIndexEntry struct in opatIO.h
|
||||
*/
|
||||
class TableIndexEntry {
|
||||
constructor() {
|
||||
this.tag = "";
|
||||
this.byteStart = 0n;
|
||||
this.byteEnd = 0n;
|
||||
this.numColumns = 0;
|
||||
this.numRows = 0;
|
||||
this.columnName = "";
|
||||
this.rowName = "";
|
||||
this.size = 0n; // Vector size of each cell
|
||||
this.reserved = null;
|
||||
}
|
||||
|
||||
static fromReader(reader) {
|
||||
const entry = new TableIndexEntry();
|
||||
entry.tag = reader.readString(8);
|
||||
entry.byteStart = reader.readUint64();
|
||||
entry.byteEnd = reader.readUint64();
|
||||
entry.numColumns = reader.readUint16();
|
||||
entry.numRows = reader.readUint16();
|
||||
entry.columnName = reader.readString(8);
|
||||
entry.rowName = reader.readString(8);
|
||||
entry.size = reader.readUint64();
|
||||
entry.reserved = reader.readBytes(12);
|
||||
return entry;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Corresponds to the OPATTable struct in opatIO.h
|
||||
*/
|
||||
class OPATTable {
|
||||
constructor() {
|
||||
this.rowValues = new Float64Array(0);
|
||||
this.columnValues = new Float64Array(0);
|
||||
this.data = new Float64Array(0);
|
||||
this.N_R = 0;
|
||||
this.N_C = 0;
|
||||
this.m_vsize = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the vector at a specific cell.
|
||||
* @param {number} row The row index.
|
||||
* @param {number} column The column index.
|
||||
* @returns {Float64Array} The vector data in the cell.
|
||||
*/
|
||||
getData(row, column) {
|
||||
if (row >= this.N_R || column >= this.N_C) {
|
||||
throw new Error("Index out of range");
|
||||
}
|
||||
const startIndex = (row * this.N_C + column) * this.m_vsize;
|
||||
return this.data.subarray(startIndex, startIndex + this.m_vsize);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a single primitive value from a cell.
|
||||
* @param {number} row
|
||||
* @param {number} column
|
||||
* @param {number} zdepth The index within the cell's vector.
|
||||
*/
|
||||
getValue(row, column, zdepth = 0) {
|
||||
if (row >= this.N_R || column >= this.N_C || zdepth >= this.m_vsize) {
|
||||
throw new Error("Index out of range");
|
||||
}
|
||||
const index = (row * this.N_C + column) * this.m_vsize + zdepth;
|
||||
return this.data[index];
|
||||
}
|
||||
|
||||
static fromReader(reader, cardEntry, tableEntry) {
|
||||
const table = new OPATTable();
|
||||
table.N_R = tableEntry.numRows;
|
||||
table.N_C = tableEntry.numColumns;
|
||||
table.m_vsize = Number(tableEntry.size); // Convert BigInt to Number
|
||||
|
||||
reader.seek(Number(cardEntry.byteStart) + Number(tableEntry.byteStart));
|
||||
|
||||
table.rowValues = reader.readFloat64Array(table.N_R);
|
||||
table.columnValues = reader.readFloat64Array(table.N_C);
|
||||
|
||||
const dataSize = table.N_R * table.N_C * table.m_vsize;
|
||||
table.data = reader.readFloat64Array(dataSize);
|
||||
|
||||
return table;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Corresponds to the DataCard struct in opatIO.h
|
||||
*/
|
||||
class DataCard {
|
||||
constructor() {
|
||||
/** @type {CardHeader} */
|
||||
this.header = null;
|
||||
/** @type {Map<string, TableIndexEntry>} */
|
||||
this.tableIndex = new Map();
|
||||
/** @type {Map<string, OPATTable>} */
|
||||
this.tableData = new Map();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a table by its tag name.
|
||||
* @param {string} tag The tag of the table to retrieve.
|
||||
* @returns {OPATTable | undefined}
|
||||
*/
|
||||
get(tag) {
|
||||
return this.tableData.get(tag);
|
||||
}
|
||||
|
||||
static fromReader(reader, cardCatalogEntry) {
|
||||
const card = new DataCard();
|
||||
reader.seek(Number(cardCatalogEntry.byteStart));
|
||||
|
||||
// 1. Read Card Header
|
||||
card.header = CardHeader.fromReader(reader);
|
||||
|
||||
// 2. Read Table Index
|
||||
reader.seek(Number(cardCatalogEntry.byteStart) + Number(card.header.indexOffset));
|
||||
for (let i = 0; i < card.header.numTables; i++) {
|
||||
const tableIndexEntry = TableIndexEntry.fromReader(reader);
|
||||
card.tableIndex.set(tableIndexEntry.tag, tableIndexEntry);
|
||||
}
|
||||
|
||||
// 3. Read all Table Data within this card
|
||||
for (const [tag, tableEntry] of card.tableIndex.entries()) {
|
||||
const table = OPATTable.fromReader(reader, cardCatalogEntry, tableEntry);
|
||||
card.tableData.set(tag, table);
|
||||
}
|
||||
|
||||
return card;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* The main class representing an entire OPAT file.
|
||||
*/
|
||||
class OPAT {
|
||||
constructor() {
|
||||
/** @type {Header} */
|
||||
this.header = null;
|
||||
/** @type {Map<string, CardCatalogEntry>} */
|
||||
this.cardCatalog = new Map();
|
||||
/** @type {Map<string, DataCard>} */
|
||||
this.cards = new Map();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a DataCard by its index vector.
|
||||
* @param {number[]} index The index vector (e.g., [1.0, 2.5]).
|
||||
* @returns {DataCard | undefined}
|
||||
*/
|
||||
get(index) {
|
||||
const key = JSON.stringify(index);
|
||||
return this.cards.get(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates and returns the bounds for each dimension.
|
||||
* @returns {Array<{min: number, max: number}>}
|
||||
*/
|
||||
getBounds() {
|
||||
const bounds = Array(this.header.numIndex).fill(null).map(() => ({
|
||||
min: Infinity,
|
||||
max: -Infinity
|
||||
}));
|
||||
|
||||
for (const entry of this.cardCatalog.values()) {
|
||||
for (let i = 0; i < this.header.numIndex; i++) {
|
||||
bounds[i].min = Math.min(bounds[i].min, entry.index[i]);
|
||||
bounds[i].max = Math.max(bounds[i].max, entry.index[i]);
|
||||
}
|
||||
}
|
||||
return bounds;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Main parsing function. Reads an ArrayBuffer and returns an OPAT object.
|
||||
* @param {ArrayBuffer} arrayBuffer The content of the .opat file.
|
||||
* @returns {OPAT} The parsed OPAT object.
|
||||
*/
|
||||
function parseOPAT(arrayBuffer) {
|
||||
const reader = new DataReader(arrayBuffer);
|
||||
const opat = new OPAT();
|
||||
|
||||
// 1. Read Header
|
||||
opat.header = Header.fromReader(reader);
|
||||
if (opat.header.magic !== "OPAT") {
|
||||
throw new Error("File is not a valid OPAT file.");
|
||||
}
|
||||
|
||||
// 2. Read Card Catalog
|
||||
reader.seek(Number(opat.header.indexOffset)); // Seek to where the catalog starts
|
||||
for (let i = 0; i < opat.header.numTables; i++) {
|
||||
const entry = CardCatalogEntry.fromReader(reader, opat.header.numIndex);
|
||||
opat.cardCatalog.set(entry.getKey(), entry);
|
||||
}
|
||||
|
||||
// 3. Read all DataCards
|
||||
for (const entry of opat.cardCatalog.values()) {
|
||||
const dataCard = DataCard.fromReader(reader, entry);
|
||||
opat.cards.set(entry.getKey(), dataCard);
|
||||
}
|
||||
|
||||
return opat;
|
||||
}
|
||||
@@ -51,6 +51,26 @@ document.addEventListener('DOMContentLoaded', async () => {
|
||||
// Initialize DOM elements
|
||||
initializeDOMElements();
|
||||
|
||||
// Initialize OPAT tab navigation
|
||||
initializeOPATTabs();
|
||||
|
||||
// Initialize OPAT UI elements
|
||||
initializeOPATElements();
|
||||
|
||||
// Initialize home screen - set home as default active category
|
||||
const homeCategory = document.querySelector('.category-item[data-category="home"]');
|
||||
const secondarySidebar = document.getElementById('secondary-sidebar');
|
||||
|
||||
if (homeCategory) {
|
||||
homeCategory.classList.add('active');
|
||||
showCategoryHomeScreen('home');
|
||||
|
||||
// Hide secondary sidebar on initial load since we start with home
|
||||
if (secondarySidebar) {
|
||||
secondarySidebar.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
// Set initial view
|
||||
showView('welcome-screen');
|
||||
|
||||
@@ -221,6 +241,8 @@ function setupEventListeners() {
|
||||
fillTargetsContent.classList.add('hidden');
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Check if bundle is signed and show warning before bundle-modifying operations
|
||||
function checkSignatureAndWarn(operation, operationName = 'operation') {
|
||||
const isSigned = currentBundle &&
|
||||
@@ -287,6 +309,7 @@ function setupEventListeners() {
|
||||
categoryItems.forEach(categoryItem => {
|
||||
categoryItem.addEventListener('click', () => {
|
||||
const selectedCategory = categoryItem.getAttribute('data-category');
|
||||
const secondarySidebar = document.getElementById('secondary-sidebar');
|
||||
|
||||
// Remove active class from all category items
|
||||
categoryItems.forEach(item => item.classList.remove('active'));
|
||||
@@ -297,17 +320,30 @@ function setupEventListeners() {
|
||||
// Hide all sidebar contents
|
||||
sidebarContents.forEach(content => content.classList.add('hidden'));
|
||||
|
||||
// Show selected category content
|
||||
const selectedContent = document.querySelector(`.sidebar-content[data-category="${selectedCategory}"]`);
|
||||
if (selectedContent) {
|
||||
selectedContent.classList.remove('hidden');
|
||||
// Show/hide secondary sidebar based on category
|
||||
if (selectedCategory === 'home') {
|
||||
// Hide the entire secondary sidebar on home screen
|
||||
if (secondarySidebar) {
|
||||
secondarySidebar.style.display = 'none';
|
||||
}
|
||||
} else {
|
||||
// Show secondary sidebar and its content for other categories
|
||||
if (secondarySidebar) {
|
||||
secondarySidebar.style.display = 'flex';
|
||||
}
|
||||
const selectedContent = document.querySelector(`.sidebar-content[data-category="${selectedCategory}"]`);
|
||||
if (selectedContent) {
|
||||
selectedContent.classList.remove('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
// Update welcome screen title based on selected category
|
||||
updateWelcomeScreen(selectedCategory);
|
||||
// Show appropriate home screen
|
||||
showCategoryHomeScreen(selectedCategory);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
|
||||
// Update welcome screen based on selected category
|
||||
function updateWelcomeScreen(category) {
|
||||
const welcomeTitle = document.querySelector('#welcome-screen h1');
|
||||
@@ -995,3 +1031,360 @@ async function reloadCurrentBundle() {
|
||||
displayBundleInfo(result.report);
|
||||
}
|
||||
}
|
||||
|
||||
// ===== OPAT FILE INSPECTOR LOGIC =====
|
||||
// OPAT File Inspector variables
|
||||
let currentOPATFile = null;
|
||||
let opatFileInput, opatBrowseBtn, opatView, opatCloseBtn;
|
||||
let opatHeaderInfo, opatAllTagsList, opatIndexSelector, opatTablesDisplay, opatTableDataContent;
|
||||
|
||||
// Helper function to hide all views
|
||||
function hideAllViews() {
|
||||
const welcomeScreen = document.getElementById('welcome-screen');
|
||||
const libpluginHome = document.getElementById('libplugin-home');
|
||||
const opatHome = document.getElementById('opat-home');
|
||||
const libconstantsHome = document.getElementById('libconstants-home');
|
||||
const serifHome = document.getElementById('serif-home');
|
||||
const opatView = document.getElementById('opat-view');
|
||||
const libpluginView = document.getElementById('libplugin-view');
|
||||
|
||||
if (welcomeScreen) welcomeScreen.classList.add('hidden');
|
||||
if (libpluginHome) libpluginHome.classList.add('hidden');
|
||||
if (opatHome) opatHome.classList.add('hidden');
|
||||
if (libconstantsHome) libconstantsHome.classList.add('hidden');
|
||||
if (serifHome) serifHome.classList.add('hidden');
|
||||
if (opatView) opatView.classList.add('hidden');
|
||||
if (libpluginView) libpluginView.classList.add('hidden');
|
||||
}
|
||||
|
||||
// Show appropriate home screen based on selected category
|
||||
function showCategoryHomeScreen(category) {
|
||||
hideAllViews();
|
||||
|
||||
switch (category) {
|
||||
case 'home':
|
||||
const welcomeScreen = document.getElementById('welcome-screen');
|
||||
if (welcomeScreen) welcomeScreen.classList.remove('hidden');
|
||||
break;
|
||||
case 'libplugin':
|
||||
const libpluginHome = document.getElementById('libplugin-home');
|
||||
if (libpluginHome) libpluginHome.classList.remove('hidden');
|
||||
break;
|
||||
case 'opat':
|
||||
const opatHome = document.getElementById('opat-home');
|
||||
if (opatHome) opatHome.classList.remove('hidden');
|
||||
break;
|
||||
case 'libconstants':
|
||||
const libconstantsHome = document.getElementById('libconstants-home');
|
||||
if (libconstantsHome) libconstantsHome.classList.remove('hidden');
|
||||
break;
|
||||
case 'serif':
|
||||
const serifHome = document.getElementById('serif-home');
|
||||
if (serifHome) serifHome.classList.remove('hidden');
|
||||
break;
|
||||
default:
|
||||
const defaultWelcome = document.getElementById('welcome-screen');
|
||||
if (defaultWelcome) defaultWelcome.classList.remove('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize OPAT UI elements in DOMContentLoaded
|
||||
function initializeOPATElements() {
|
||||
opatFileInput = document.getElementById('opat-file-input');
|
||||
opatBrowseBtn = document.getElementById('opat-browse-btn');
|
||||
opatView = document.getElementById('opat-view');
|
||||
opatCloseBtn = document.getElementById('opat-close-btn');
|
||||
opatHeaderInfo = document.getElementById('opat-header-info');
|
||||
opatAllTagsList = document.getElementById('opat-all-tags-list');
|
||||
opatIndexSelector = document.getElementById('opat-index-selector');
|
||||
opatTablesDisplay = document.getElementById('opat-tables-display');
|
||||
opatTableDataContent = document.getElementById('opat-table-data-content');
|
||||
|
||||
// Event listeners
|
||||
opatBrowseBtn.addEventListener('click', () => opatFileInput.click());
|
||||
opatFileInput.addEventListener('change', handleOPATFileSelection);
|
||||
opatIndexSelector.addEventListener('change', handleIndexVectorChange);
|
||||
opatCloseBtn.addEventListener('click', closeOPATFile);
|
||||
|
||||
// Initialize OPAT tab navigation
|
||||
initializeOPATTabs();
|
||||
|
||||
// Add window resize listener to update table heights
|
||||
window.updateTableHeights = function() {
|
||||
const newHeight = Math.max(300, window.innerHeight - 450);
|
||||
|
||||
// Target the main table containers
|
||||
const containers = document.querySelectorAll('.opat-table-container');
|
||||
containers.forEach((container, index) => {
|
||||
container.style.setProperty('height', newHeight + 'px', 'important');
|
||||
});
|
||||
|
||||
// Also target the scroll wrappers
|
||||
const scrollWrappers = document.querySelectorAll('.table-scroll-wrapper');
|
||||
scrollWrappers.forEach((wrapper, index) => {
|
||||
wrapper.style.setProperty('height', '100%', 'important');
|
||||
});
|
||||
};
|
||||
|
||||
window.addEventListener('resize', () => {
|
||||
console.log('Window resize event fired');
|
||||
window.updateTableHeights();
|
||||
});
|
||||
|
||||
// Also update on initial load and when tables are displayed
|
||||
setTimeout(window.updateTableHeights, 100);
|
||||
}
|
||||
|
||||
function closeOPATFile() {
|
||||
currentOPATFile = null;
|
||||
opatFileInput.value = '';
|
||||
resetOPATViewerState();
|
||||
hideAllViews();
|
||||
document.getElementById('welcome-screen').classList.remove('hidden');
|
||||
}
|
||||
|
||||
function resetOPATViewerState() {
|
||||
// Clear all OPAT content areas
|
||||
if (opatHeaderInfo) opatHeaderInfo.innerHTML = '';
|
||||
if (opatAllTagsList) opatAllTagsList.innerHTML = '';
|
||||
if (opatIndexSelector) {
|
||||
opatIndexSelector.innerHTML = '<option value="">-- Select an Index Vector --</option>';
|
||||
}
|
||||
if (opatTablesDisplay) opatTablesDisplay.innerHTML = '';
|
||||
if (opatTableDataContent) {
|
||||
opatTableDataContent.innerHTML = '<p class="opat-placeholder">Click on a table from the \'Data Card Explorer\' above to view its data here.</p>';
|
||||
}
|
||||
|
||||
// Reset to File Information tab
|
||||
const opatTabLinks = document.querySelectorAll('#opat-view .tab-link');
|
||||
const opatTabPanes = document.querySelectorAll('#opat-tab-content .tab-pane');
|
||||
|
||||
opatTabLinks.forEach(link => link.classList.remove('active'));
|
||||
opatTabPanes.forEach(pane => pane.classList.add('hidden'));
|
||||
|
||||
// Activate File Information tab
|
||||
const fileInfoTab = document.querySelector('[data-tab="opat-overview-tab"]');
|
||||
const fileInfoPane = document.getElementById('opat-overview-tab');
|
||||
if (fileInfoTab) fileInfoTab.classList.add('active');
|
||||
if (fileInfoPane) fileInfoPane.classList.remove('hidden');
|
||||
}
|
||||
|
||||
function initializeOPATTabs() {
|
||||
const opatTabLinks = document.querySelectorAll('#opat-view .tab-link');
|
||||
const opatTabPanes = document.querySelectorAll('#opat-tab-content .tab-pane');
|
||||
|
||||
opatTabLinks.forEach(link => {
|
||||
link.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
const targetTab = link.getAttribute('data-tab');
|
||||
|
||||
// Remove active class from all tabs and panes
|
||||
opatTabLinks.forEach(l => l.classList.remove('active'));
|
||||
opatTabPanes.forEach(p => p.classList.add('hidden'));
|
||||
|
||||
// Add active class to clicked tab and show corresponding pane
|
||||
link.classList.add('active');
|
||||
document.getElementById(targetTab).classList.remove('hidden');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
async function handleOPATFileSelection(event) {
|
||||
const file = event.target.files[0];
|
||||
if (!file) return;
|
||||
|
||||
try {
|
||||
// Reset viewer state before loading new file
|
||||
resetOPATViewerState();
|
||||
|
||||
const arrayBuffer = await file.arrayBuffer();
|
||||
currentOPATFile = parseOPAT(arrayBuffer);
|
||||
|
||||
displayOPATFileInfo();
|
||||
populateIndexSelector();
|
||||
|
||||
// Show OPAT view and hide other views
|
||||
hideAllViews();
|
||||
opatView.classList.remove('hidden');
|
||||
|
||||
// Update title with filename
|
||||
document.getElementById('opat-title').textContent = `OPAT File Inspector - ${file.name}`;
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error parsing OPAT file:', error);
|
||||
showModal('Error', `Failed to parse OPAT file: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
function displayOPATFileInfo() {
|
||||
if (!currentOPATFile) return;
|
||||
|
||||
const header = currentOPATFile.header;
|
||||
opatHeaderInfo.innerHTML = `
|
||||
<div class="opat-info-section">
|
||||
<h4 class="opat-section-title">Header Information</h4>
|
||||
<div class="info-grid">
|
||||
<p><strong>Magic:</strong> ${header.magic}</p>
|
||||
<p><strong>Version:</strong> ${header.version}</p>
|
||||
<p><strong>Number of Tables:</strong> ${header.numTables}</p>
|
||||
<p><strong>Header Size:</strong> ${header.headerSize} bytes</p>
|
||||
<p><strong>Index Offset:</strong> ${header.indexOffset}</p>
|
||||
<p><strong>Creation Date:</strong> ${header.creationDate}</p>
|
||||
<p><strong>Source Info:</strong> ${header.sourceInfo}</p>
|
||||
<p><strong>Comment:</strong> ${header.comment || 'None'}</p>
|
||||
<p><strong>Number of Indices:</strong> ${header.numIndex}</p>
|
||||
<p><strong>Hash Precision:</strong> ${header.hashPrecision}</p>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Display all unique table tags
|
||||
displayAllTableTags();
|
||||
}
|
||||
|
||||
function displayAllTableTags() {
|
||||
if (!currentOPATFile) return;
|
||||
|
||||
const allTags = new Set();
|
||||
for (const card of currentOPATFile.cards.values()) {
|
||||
for (const tag of card.tableIndex.keys()) {
|
||||
allTags.add(tag);
|
||||
}
|
||||
}
|
||||
|
||||
opatAllTagsList.innerHTML = '';
|
||||
Array.from(allTags).sort().forEach(tag => {
|
||||
const li = document.createElement('li');
|
||||
li.textContent = tag;
|
||||
opatAllTagsList.appendChild(li);
|
||||
});
|
||||
}
|
||||
|
||||
function populateIndexSelector() {
|
||||
if (!currentOPATFile) return;
|
||||
|
||||
opatIndexSelector.innerHTML = '<option value="">-- Select an index vector --</option>';
|
||||
|
||||
for (const [key, entry] of currentOPATFile.cardCatalog.entries()) {
|
||||
const option = document.createElement('option');
|
||||
option.value = key;
|
||||
option.textContent = `[${entry.index.join(', ')}]`;
|
||||
opatIndexSelector.appendChild(option);
|
||||
}
|
||||
}
|
||||
|
||||
function handleIndexVectorChange() {
|
||||
const selectedKey = opatIndexSelector.value;
|
||||
if (!selectedKey || !currentOPATFile) {
|
||||
opatTablesDisplay.innerHTML = '';
|
||||
return;
|
||||
}
|
||||
|
||||
const card = currentOPATFile.cards.get(selectedKey);
|
||||
if (!card) return;
|
||||
|
||||
opatTablesDisplay.innerHTML = '';
|
||||
|
||||
for (const [tag, tableEntry] of card.tableIndex.entries()) {
|
||||
const tableInfo = document.createElement('div');
|
||||
tableInfo.className = 'opat-table-info';
|
||||
tableInfo.innerHTML = `
|
||||
<div class="opat-table-tag">${tag}</div>
|
||||
<div class="opat-table-details">
|
||||
Rows: ${tableEntry.numRows}, Columns: ${tableEntry.numColumns}<br>
|
||||
Row Name: ${tableEntry.rowName}, Column Name: ${tableEntry.columnName}
|
||||
</div>
|
||||
`;
|
||||
|
||||
tableInfo.addEventListener('click', () => {
|
||||
const table = card.tableData.get(tag);
|
||||
displayTableData(table, tag);
|
||||
});
|
||||
|
||||
opatTablesDisplay.appendChild(tableInfo);
|
||||
}
|
||||
}
|
||||
|
||||
function displayTableData(table, tag, showAll = false) {
|
||||
if (!table) {
|
||||
opatTableDataContent.innerHTML = '<p class="opat-placeholder">Table not found.</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
let html = `<div class="opat-table-title"><span class="opat-table-tag-highlight">${tag}</span> Table Data</div>`;
|
||||
html += `<p><strong>Dimensions:</strong> ${table.N_R} rows × ${table.N_C} columns × ${table.m_vsize} values per cell</p>`;
|
||||
|
||||
if (table.N_R > 0 && table.N_C > 0) {
|
||||
if (table.m_vsize === 0 || table.data.length === 0) {
|
||||
html += '<p><strong>Note:</strong> This table has no data values (m_vsize = 0 or empty data array).</p>';
|
||||
html += '<p>The table structure exists but contains no numerical data to display.</p>';
|
||||
} else {
|
||||
// Add show all/show less toggle buttons
|
||||
if (table.N_R > 50) {
|
||||
html += '<div class="table-controls">';
|
||||
if (!showAll) {
|
||||
html += `<button class="show-all-btn" onclick="displayTableData(currentOPATFile.cards.get('${opatIndexSelector.value}').tableData.get('${tag}'), '${tag}', true)">Show All ${table.N_R} Rows</button>`;
|
||||
} else {
|
||||
html += `<button class="show-less-btn" onclick="displayTableData(currentOPATFile.cards.get('${opatIndexSelector.value}').tableData.get('${tag}'), '${tag}', false)">Show First 50 Rows</button>`;
|
||||
}
|
||||
html += '</div>';
|
||||
}
|
||||
|
||||
html += '<div class="opat-table-container">';
|
||||
html += '<div class="table-scroll-wrapper">';
|
||||
html += '<table class="opat-data-table">';
|
||||
|
||||
// Header row
|
||||
html += '<thead><tr><th class="corner-cell"></th>';
|
||||
for (let c = 0; c < table.N_C; c++) {
|
||||
html += `<th>${table.columnValues[c].toFixed(3)}</th>`;
|
||||
}
|
||||
html += '</tr></thead>';
|
||||
|
||||
// Data rows
|
||||
html += '<tbody>';
|
||||
const rowsToShow = showAll ? table.N_R : Math.min(table.N_R, 50);
|
||||
for (let r = 0; r < rowsToShow; r++) {
|
||||
html += '<tr>';
|
||||
html += `<th class="row-header">${table.rowValues[r].toFixed(3)}</th>`;
|
||||
for (let c = 0; c < table.N_C; c++) {
|
||||
try {
|
||||
const value = table.getValue(r, c, 0); // Get first value in cell
|
||||
html += `<td>${value.toFixed(6)}</td>`;
|
||||
} catch (error) {
|
||||
html += `<td>N/A</td>`;
|
||||
}
|
||||
}
|
||||
html += '</tr>';
|
||||
}
|
||||
html += '</tbody>';
|
||||
html += '</table>';
|
||||
html += '</div></div>';
|
||||
|
||||
if (table.N_R > 50 && !showAll) {
|
||||
html += `<p><em>Showing first 50 rows of ${table.N_R} total rows.</em></p>`;
|
||||
} else if (showAll && table.N_R > 50) {
|
||||
html += `<p><em>Showing all ${table.N_R} rows.</em></p>`;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
html += '<p>No data to display.</p>';
|
||||
}
|
||||
|
||||
opatTableDataContent.innerHTML = html;
|
||||
|
||||
// Auto-switch to Data Explorer tab when displaying data
|
||||
const explorerTab = document.querySelector('[data-tab="opat-explorer-tab"]');
|
||||
if (explorerTab) {
|
||||
explorerTab.click();
|
||||
}
|
||||
|
||||
// Update table heights after table is rendered
|
||||
setTimeout(() => {
|
||||
if (window.updateTableHeights) {
|
||||
window.updateTableHeights();
|
||||
}
|
||||
}, 50);
|
||||
}
|
||||
|
||||
@@ -504,18 +504,403 @@ body.dark-mode .info-button:hover {
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
min-width: 0;
|
||||
padding: 0;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
#welcome-screen {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
background: linear-gradient(135deg, #f8fafc 0%, #e2e8f0 100%);
|
||||
}
|
||||
|
||||
.welcome-hero {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
min-height: 300px;
|
||||
background: linear-gradient(135deg, #1e293b 0%, #334155 100%);
|
||||
color: white;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.welcome-logo {
|
||||
text-align: center;
|
||||
color: var(--text-light);
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.star-icon-large {
|
||||
font-size: 4rem;
|
||||
color: #fbbf24;
|
||||
margin-bottom: 1rem;
|
||||
text-shadow: 0 0 20px rgba(251, 191, 36, 0.5);
|
||||
}
|
||||
|
||||
.welcome-title {
|
||||
font-size: 3.5rem;
|
||||
font-weight: 700;
|
||||
margin: 0;
|
||||
letter-spacing: 0.1em;
|
||||
background: linear-gradient(45deg, #fbbf24, #f59e0b);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
|
||||
.welcome-subtitle {
|
||||
font-size: 1.2rem;
|
||||
margin: 0.5rem 0 0 0;
|
||||
color: #cbd5e1;
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
.welcome-content {
|
||||
flex: 1;
|
||||
padding: 0 3rem 3rem 3rem;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.welcome-section h2 {
|
||||
color: #1e293b;
|
||||
font-size: 2rem;
|
||||
margin-bottom: 1rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.welcome-section p {
|
||||
color: #64748b;
|
||||
font-size: 1.1rem;
|
||||
text-align: center;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.feature-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
gap: 1.5rem;
|
||||
margin-bottom: 3rem;
|
||||
}
|
||||
|
||||
.feature-card {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 1rem;
|
||||
padding: 1.5rem;
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
|
||||
transition: all 0.3s ease;
|
||||
border: 1px solid #e2e8f0;
|
||||
}
|
||||
|
||||
.feature-card:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 25px -5px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.feature-icon {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: white;
|
||||
font-weight: bold;
|
||||
font-size: 1.2rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.feature-info h3 {
|
||||
color: #1e293b;
|
||||
font-size: 1.2rem;
|
||||
margin: 0 0 0.5rem 0;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.feature-info p {
|
||||
color: #64748b;
|
||||
font-size: 0.95rem;
|
||||
margin: 0;
|
||||
line-height: 1.5;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.welcome-tips {
|
||||
background: white;
|
||||
padding: 2rem;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
|
||||
border: 1px solid #e2e8f0;
|
||||
}
|
||||
|
||||
.welcome-tips h3 {
|
||||
color: #1e293b;
|
||||
font-size: 1.4rem;
|
||||
margin: 0 0 1rem 0;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.welcome-tips ul {
|
||||
color: #64748b;
|
||||
font-size: 1rem;
|
||||
line-height: 1.6;
|
||||
margin: 0;
|
||||
padding-left: 1.5rem;
|
||||
}
|
||||
|
||||
.welcome-tips li {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
/* Category-specific home screens */
|
||||
.home-screen {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.category-hero {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 250px;
|
||||
background: linear-gradient(135deg, #1e293b 0%, #334155 100%);
|
||||
color: white;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.category-hero-content {
|
||||
text-align: center;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.category-icon-large {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
border-radius: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: white;
|
||||
font-weight: bold;
|
||||
font-size: 2.5rem;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.category-title {
|
||||
font-size: 2.5rem;
|
||||
font-weight: 700;
|
||||
margin: 0;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.category-subtitle {
|
||||
font-size: 1.1rem;
|
||||
margin: 0;
|
||||
color: #cbd5e1;
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
.category-content {
|
||||
flex: 1;
|
||||
padding: 0 3rem 3rem 3rem;
|
||||
max-width: 1000px;
|
||||
margin: 0 auto;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.feature-section h2 {
|
||||
color: #1e293b;
|
||||
font-size: 1.8rem;
|
||||
margin-bottom: 1.5rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.action-cards {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
||||
gap: 1.5rem;
|
||||
margin-bottom: 3rem;
|
||||
}
|
||||
|
||||
.action-card {
|
||||
padding: 2rem;
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
|
||||
transition: all 0.3s ease;
|
||||
border: 1px solid #e2e8f0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.action-card:hover {
|
||||
transform: translateY(-4px);
|
||||
box-shadow: 0 12px 32px -8px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.action-card h3 {
|
||||
color: #1e293b;
|
||||
font-size: 1.3rem;
|
||||
margin: 0 0 1rem 0;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.action-card p {
|
||||
color: #64748b;
|
||||
font-size: 1rem;
|
||||
margin: 0;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.info-section {
|
||||
background: white;
|
||||
padding: 2rem;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
|
||||
border: 1px solid #e2e8f0;
|
||||
}
|
||||
|
||||
.info-section h3 {
|
||||
color: #1e293b;
|
||||
font-size: 1.4rem;
|
||||
margin: 0 0 1rem 0;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.info-section p {
|
||||
color: #64748b;
|
||||
font-size: 1rem;
|
||||
line-height: 1.6;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* Dark mode support for home screens */
|
||||
body.dark-mode .home-screen {
|
||||
background: linear-gradient(135deg, #0f172a 0%, #1e293b 100%);
|
||||
}
|
||||
|
||||
body.dark-mode .category-hero {
|
||||
background: linear-gradient(135deg, #0f172a 0%, #1e293b 100%);
|
||||
}
|
||||
|
||||
body.dark-mode .feature-section h2,
|
||||
body.dark-mode .category-title {
|
||||
color: #f1f5f9;
|
||||
}
|
||||
|
||||
body.dark-mode .category-subtitle {
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
body.dark-mode .action-card,
|
||||
body.dark-mode .info-section,
|
||||
body.dark-mode .feature-card,
|
||||
body.dark-mode .welcome-tips {
|
||||
background: #1e293b;
|
||||
border-color: #334155;
|
||||
}
|
||||
|
||||
body.dark-mode .action-card h3,
|
||||
body.dark-mode .info-section h3,
|
||||
body.dark-mode .feature-info h3,
|
||||
body.dark-mode .welcome-tips h3 {
|
||||
color: #f1f5f9;
|
||||
}
|
||||
|
||||
body.dark-mode .action-card p,
|
||||
body.dark-mode .info-section p,
|
||||
body.dark-mode .feature-info p,
|
||||
body.dark-mode .welcome-tips ul {
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
body.dark-mode .welcome-section h2 {
|
||||
color: #f1f5f9;
|
||||
}
|
||||
|
||||
body.dark-mode .welcome-section p {
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
/* Custom scrollbar styling for all views */
|
||||
::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: #f1f5f9;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: #cbd5e1;
|
||||
border-radius: 4px;
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: #94a3b8;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-corner {
|
||||
background: #f1f5f9;
|
||||
}
|
||||
|
||||
/* Dark mode scrollbar styling */
|
||||
body.dark-mode ::-webkit-scrollbar-track {
|
||||
background: #1e293b;
|
||||
}
|
||||
|
||||
body.dark-mode ::-webkit-scrollbar-thumb {
|
||||
background: #475569;
|
||||
}
|
||||
|
||||
body.dark-mode ::-webkit-scrollbar-thumb:hover {
|
||||
background: #64748b;
|
||||
}
|
||||
|
||||
body.dark-mode ::-webkit-scrollbar-corner {
|
||||
background: #1e293b;
|
||||
}
|
||||
|
||||
/* Specific scrollbar styling for table containers */
|
||||
.opat-table-container ::-webkit-scrollbar {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
}
|
||||
|
||||
.opat-table-container ::-webkit-scrollbar-track {
|
||||
background: #f8fafc;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.opat-table-container ::-webkit-scrollbar-thumb {
|
||||
background: #e2e8f0;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.opat-table-container ::-webkit-scrollbar-thumb:hover {
|
||||
background: #cbd5e1;
|
||||
}
|
||||
|
||||
body.dark-mode .opat-table-container ::-webkit-scrollbar-track {
|
||||
background: #0f172a;
|
||||
}
|
||||
|
||||
body.dark-mode .opat-table-container ::-webkit-scrollbar-thumb {
|
||||
background: #334155;
|
||||
}
|
||||
|
||||
body.dark-mode .opat-table-container ::-webkit-scrollbar-thumb:hover {
|
||||
background: #475569;
|
||||
}
|
||||
|
||||
#welcome-screen h1 {
|
||||
@@ -1178,11 +1563,11 @@ body.dark-mode .fill-header p {
|
||||
}
|
||||
|
||||
#fill-progress-container {
|
||||
margin-top: 20px;
|
||||
padding: 16px;
|
||||
background-color: var(--sidebar-bg);
|
||||
margin-bottom: 20px;
|
||||
padding: 20px 30px;
|
||||
background-color: #f8f9fa;
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--border-color);
|
||||
border: 1px solid #e9ecef;
|
||||
}
|
||||
|
||||
#fill-progress-content {
|
||||
@@ -1530,6 +1915,346 @@ body.dark-mode .info-tab-pane a {
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
/* OPAT File Inspector Styling */
|
||||
#opat-tab-content {
|
||||
padding: 20px;
|
||||
height: calc(100vh - 200px);
|
||||
overflow-y: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
#opat-tab-content .tab-pane {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.opat-table-container {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
height: calc(100vh - 450px);
|
||||
}
|
||||
|
||||
.table-scroll-wrapper {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-height: 300px;
|
||||
overflow: auto;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
#opat-tab-content .tab-pane {
|
||||
padding: 0;
|
||||
}
|
||||
.opat-section {
|
||||
margin-bottom: 20px;
|
||||
padding: 16px;
|
||||
background: white;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
body.dark-mode .opat-section {
|
||||
background: #4b5563;
|
||||
border: 1px solid #6b7280;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.opat-label {
|
||||
display: block;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 500;
|
||||
color: #374151;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
body.dark-mode .opat-label {
|
||||
color: #f3f4f6;
|
||||
}
|
||||
|
||||
.opat-file-input {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.opat-content-wrapper {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.opat-info-section {
|
||||
margin: 0 20px 24px 20px;
|
||||
padding: 20px 30px;
|
||||
background: white;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
body.dark-mode .opat-info-section {
|
||||
background: #4b5563;
|
||||
border: 1px solid #6b7280;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.opat-section-title {
|
||||
font-size: 1.1rem;
|
||||
font-weight: 600;
|
||||
color: #1f2937;
|
||||
margin: 0 0 12px 0;
|
||||
padding-bottom: 8px;
|
||||
border-bottom: 1px solid #e5e7eb;
|
||||
}
|
||||
|
||||
body.dark-mode .opat-section-title {
|
||||
color: #f3f4f6;
|
||||
border-bottom: 1px solid #6b7280;
|
||||
}
|
||||
|
||||
.opat-info-content {
|
||||
font-size: 0.85rem;
|
||||
color: #6b7280;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
body.dark-mode .opat-info-content {
|
||||
color: #d1d5db;
|
||||
}
|
||||
|
||||
.opat-tags-list {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.opat-tags-list li {
|
||||
padding: 6px 12px;
|
||||
margin: 4px 0;
|
||||
background: #f9fafb;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 6px;
|
||||
font-size: 0.85rem;
|
||||
font-family: monospace;
|
||||
color: #059669;
|
||||
}
|
||||
|
||||
body.dark-mode .opat-tags-list li {
|
||||
background: #374151;
|
||||
border: 1px solid #4b5563;
|
||||
color: #10b981;
|
||||
}
|
||||
|
||||
.opat-selector-group {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.opat-select {
|
||||
width: 100%;
|
||||
padding: 8px 12px;
|
||||
background: white;
|
||||
border: 1px solid #d1d5db;
|
||||
border-radius: 6px;
|
||||
font-size: 0.9rem;
|
||||
color: #374151;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
body.dark-mode .opat-select {
|
||||
background: #374151;
|
||||
border: 1px solid #4b5563;
|
||||
color: #f3f4f6;
|
||||
}
|
||||
|
||||
.opat-tables-display {
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.opat-table-info {
|
||||
padding: 12px;
|
||||
margin: 8px 0;
|
||||
background: #f8f9fa;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.opat-table-info:hover {
|
||||
background: #e5e7eb;
|
||||
border-color: #3b82f6;
|
||||
}
|
||||
|
||||
body.dark-mode .opat-table-info {
|
||||
background: #374151;
|
||||
border: 1px solid #4b5563;
|
||||
}
|
||||
|
||||
body.dark-mode .opat-table-info:hover {
|
||||
background: #4b5563;
|
||||
border-color: #3b82f6;
|
||||
}
|
||||
|
||||
.opat-table-tag {
|
||||
font-weight: 600;
|
||||
color: #059669;
|
||||
font-family: monospace;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
body.dark-mode .opat-table-tag {
|
||||
color: #10b981;
|
||||
}
|
||||
|
||||
.opat-table-details {
|
||||
font-size: 0.8rem;
|
||||
color: #6b7280;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
body.dark-mode .opat-table-details {
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
.opat-table-viewer {
|
||||
max-height: 400px;
|
||||
overflow: auto;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 6px;
|
||||
background: white;
|
||||
}
|
||||
|
||||
body.dark-mode .opat-table-viewer {
|
||||
border: 1px solid #4b5563;
|
||||
background: #374151;
|
||||
}
|
||||
|
||||
.opat-placeholder {
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
color: #9ca3af;
|
||||
font-style: italic;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
body.dark-mode .opat-placeholder {
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.opat-data-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 0.8rem;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.opat-data-table th,
|
||||
.opat-data-table td {
|
||||
padding: 6px 8px;
|
||||
border: 1px solid #e5e7eb;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
body.dark-mode .opat-data-table th,
|
||||
body.dark-mode .opat-data-table td {
|
||||
border: 1px solid #4b5563;
|
||||
}
|
||||
|
||||
.opat-data-table th {
|
||||
background: #f8f9fa;
|
||||
font-weight: 600;
|
||||
color: #374151;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
body.dark-mode .opat-data-table th {
|
||||
background: #4b5563;
|
||||
color: #f3f4f6;
|
||||
}
|
||||
|
||||
.opat-data-table th.row-header {
|
||||
background-color: #f8fafc;
|
||||
font-weight: 600;
|
||||
text-align: right;
|
||||
padding-right: 12px;
|
||||
}
|
||||
|
||||
body.dark-mode .opat-data-table th.row-header {
|
||||
background: #4b5563;
|
||||
}
|
||||
|
||||
.table-controls {
|
||||
margin: 1rem 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.show-all-btn, .show-less-btn {
|
||||
background-color: #3b82f6;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 8px 16px;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 500;
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
|
||||
.show-all-btn:hover, .show-less-btn:hover {
|
||||
background-color: #2563eb;
|
||||
}
|
||||
|
||||
.show-less-btn {
|
||||
background-color: #6b7280;
|
||||
}
|
||||
|
||||
.show-less-btn:hover {
|
||||
background-color: #4b5563;
|
||||
}
|
||||
|
||||
.opat-data-table .corner-cell {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 2;
|
||||
background: #e5e7eb;
|
||||
}
|
||||
|
||||
body.dark-mode .opat-data-table .corner-cell {
|
||||
background: #6b7280;
|
||||
}
|
||||
|
||||
.opat-table-title {
|
||||
padding: 12px 16px;
|
||||
margin: 0;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
color: #1f2937;
|
||||
background: #f8f9fa;
|
||||
border-bottom: 1px solid #e5e7eb;
|
||||
}
|
||||
|
||||
body.dark-mode .opat-table-title {
|
||||
color: #f3f4f6;
|
||||
background: #4b5563;
|
||||
border-bottom: 1px solid #6b7280;
|
||||
}
|
||||
|
||||
.opat-table-tag-highlight {
|
||||
color: #059669;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
body.dark-mode .opat-table-tag-highlight {
|
||||
color: #10b981;
|
||||
}
|
||||
|
||||
.info-tab-pane ul {
|
||||
margin: 12px 0;
|
||||
padding-left: 20px;
|
||||
|
||||
Reference in New Issue
Block a user