This commit is contained in:
Skylar Grant 2025-11-05 20:49:41 -05:00
parent a57cae4600
commit 549ecbb4e8
44 changed files with 1245 additions and 15 deletions

BIN
.DS_Store vendored Normal file

Binary file not shown.

BIN
src/assets/.DS_Store vendored Normal file

Binary file not shown.

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

View File

@ -0,0 +1,3 @@
[
"assets/backgrounds/bg1.jpg"
]

View File

@ -0,0 +1,60 @@
{
"settings": {
"title": "My Startpage",
"backgroundImgPath": "/src/backgrounds/bg1.jpg",
"headerBgColor": "rgba(0, 0, 0, 0.5)",
"headerOutlineColor": "rgba(255, 255, 255, 0.7)",
"headerOutlineThickness": 2,
"cardBgColor": "rgba(255, 255, 255, 0.8)",
"cornerRadius": 10,
"openLinksInNewTab": true,
"searchEngine": {
"name": "DuckDuckGo",
"url": "https://duckduckgo.com/?q="
}
},
"cards": [
{
"title": "Category One",
"iconPath": "/src/icons/category1.png",
"links": [
{
"name": "Example Link 1",
"url": "https://example1.com"
},
{
"name": "Example Link 2",
"url": "https://example2.com"
}
]
},
{
"title": "Category Two",
"iconPath": "/src/icons/category2.png",
"links": [
{
"name": "Example Link A",
"url": "https://exampleA.com"
},
{
"name": "Example Link B",
"url": "https://exampleB.com"
}
]
},
{
"title": "Category Three",
"iconPath": "/src/icons/category3.png",
"links": [
{
"name": "Example Link X",
"url": "https://exampleX.com"
},
{
"name": "Example Link Y",
"url": "https://exampleY.com"
}
]
}
]
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 0 B

After

Width:  |  Height:  |  Size: 304 KiB

55
src/assets/images/fox.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 14 KiB

View File

@ -0,0 +1,100 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
inkscape:version="1.0 (4035a4fb49, 2020-05-01)"
sodipodi:docname="google.svg"
id="svg16"
version="1.1"
height="52"
width="52"
viewBox="0 0 5.2 5.2">
<metadata
id="metadata22">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title />
</cc:Work>
</rdf:RDF>
</metadata>
<defs
id="defs20">
<inkscape:path-effect
only_selected="false"
apply_with_weight="true"
apply_no_weight="true"
helper_size="0"
steps="2"
weight="33.333333"
lpeversion="1"
is_visible="true"
id="path-effect1925"
effect="bspline" />
<inkscape:path-effect
only_selected="false"
apply_with_weight="true"
apply_no_weight="true"
helper_size="0"
steps="2"
weight="33.333333"
lpeversion="1"
is_visible="true"
id="path-effect885"
effect="bspline" />
</defs>
<sodipodi:namedview
inkscape:current-layer="svg16"
inkscape:window-maximized="1"
inkscape:window-y="0"
inkscape:window-x="0"
inkscape:cy="24.110555"
inkscape:cx="46.005708"
inkscape:zoom="7.989887"
showgrid="false"
id="namedview18"
inkscape:window-height="704"
inkscape:window-width="1366"
inkscape:pageshadow="2"
inkscape:pageopacity="0"
guidetolerance="10"
gridtolerance="10"
objecttolerance="10"
borderopacity="1"
bordercolor="#666666"
pagecolor="#232929"
inkscape:document-rotation="0" />
<g
transform="matrix(0.92846546,0,0,0.92479909,0.17492131,0.19753118)"
style="stroke:#6c8dac;stroke-width:0.48541465;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="g1929">
<path
style="fill:#ff8080;fill-opacity:0;stroke:#6c8dac;stroke-width:0.48541465;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:stroke markers fill"
id="path1911"
sodipodi:type="arc"
sodipodi:cx="2.4996181"
sodipodi:cy="2.6108518"
sodipodi:rx="2.1740921"
sodipodi:ry="2.1773813"
sodipodi:start="6.2744429"
sodipodi:end="5.33678"
sodipodi:open="true"
sodipodi:arc-type="arc"
d="M 4.673627,2.5918165 A 2.1740921,2.1773813 0 0 1 3.0230721,4.7241798 2.1740921,2.1773813 0 0 1 0.56877939,3.6116325 2.1740921,2.1773813 0 0 1 1.0791939,0.96243588 2.1740921,2.1773813 0 0 1 3.7705995,0.84430035" />
<path
style="fill:none;stroke:#6c8dac;stroke-width:0.48541465;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 2.5940139,2.6155787 c 0,0 2.0796131,-0.023762 2.0796131,-0.023762"
id="path1923"
inkscape:path-effect="#path-effect1925"
inkscape:original-d="M 2.5940139,2.6155787 4.673627,2.5918165"
sodipodi:nodetypes="cc" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.4 KiB

View File

@ -0,0 +1,89 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
inkscape:version="1.0 (4035a4fb49, 2020-05-01)"
sodipodi:docname="search.svg"
id="svg12"
version="1.1"
class="feather feather-crosshair"
stroke-linejoin="round"
stroke-linecap="round"
stroke-width="2"
stroke="#1789D0"
fill="none"
viewBox="0 0 24 24"
height="48"
width="48">
<metadata
id="metadata18">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
</cc:Work>
</rdf:RDF>
</metadata>
<defs
id="defs16" />
<sodipodi:namedview
inkscape:current-layer="svg12"
inkscape:window-maximized="0"
inkscape:window-y="112"
inkscape:window-x="317"
inkscape:cy="24"
inkscape:cx="24"
inkscape:zoom="5.6041667"
showgrid="false"
id="namedview14"
inkscape:window-height="480"
inkscape:window-width="733"
inkscape:pageshadow="2"
inkscape:pageopacity="0"
guidetolerance="10"
gridtolerance="10"
objecttolerance="10"
borderopacity="1"
bordercolor="#666666"
pagecolor="#ffffff" />
<g
transform="matrix(1.0346466,0,0,1.0346466,-0.79687178,-0.0346466)"
id="g25">
<circle
cx="12"
cy="12"
r="10"
id="circle2" />
<line
x1="22"
y1="12"
x2="18"
y2="12"
id="line4" />
<line
x1="6"
y1="12"
x2="2"
y2="12"
id="line6" />
<line
x1="12"
y1="6"
x2="12"
y2="2"
id="line8" />
<line
x1="12"
y1="22"
x2="12"
y2="18"
id="line10" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640"><!--!Font Awesome Free 7.1.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc.--><path d="M32 432L32 496C32 522.5 53.5 544 80 544L130 544C170.6 544 210.4 533 245.2 512.1L378.7 432L285.4 432L220.5 470.9C206.5 479.3 191.6 485.6 176 489.9L176 423.5C185.7 417.9 193.8 409.8 199.4 400.1L398.2 400.1C465.3 400.1 526 379.5 568.5 329.5C573.1 324.1 573.1 316.2 568.5 310.7C526 260.6 465.3 240.1 398.2 240.1L199.4 240.1C193.8 230.4 185.7 222.3 176 216.7L176 150.3C191.6 154.5 206.6 160.9 220.5 169.3L285.4 208.2L378.7 208.2L245.2 128.1C210.4 107 170.6 96 130 96L80 96C53.5 96 32 117.5 32 144L32 432zM128 432L128 496L80 496L80 432L128 432zM128 144L128 208L80 208L80 144L128 144zM448 288C456.8 288 464 295.2 464 304L464 336C464 344.8 456.8 352 448 352C439.2 352 432 344.8 432 336L432 304C432 295.2 439.2 288 448 288z"/></svg>

After

Width:  |  Height:  |  Size: 945 B

View File

@ -0,0 +1,211 @@
@font-face {
font-family: iosevka-light;
src: url('fonts/iosevka_ttf/iosevka-light.ttf');
}
html{
display: flex;
flex-flow: row nowrap;
justify-content: center;
align-content: center;
align-items: center;
height: 100%;
width: 100%;
margin: 0;
padding: 0;
background:#1b1b1b;
}
body {
/* background: #263238; */
background-image: url('/src/backgrounds/bg1.jpg');
background-size: cover;
background-position: center;
color: #ffa31a;
height: 100%;
width: 100%;
margin: 0;
padding: 0;
}
.container {
display: flex;
flex-direction: column;
height: 100%;
width: 100%;
justify-content: center;
}
.head {
display: flex;
flex-direction: column;
}
.category {
display: flex;
flex-direction: column;
width: 20%;
font-family: iosevka-light;
}
.title {
font-size: 20px;
text-align: center;
background-color: #292929;
color: #ffa31a;
border-radius: 3px;
box-shadow: 0px 4px 5px #171D20;
align-items: center;
padding-top: 10px;
padding-bottom: 10px;
margin-bottom: 0px;
border: #ffa31a 1px;
border-style: solid;
}
.bookmark {
display: flex;
justify-content: space-evenly;
}
.bookmarks {
display: flex;
flex-direction: column;
border: #191C1E;
border-radius: 3px;
box-shadow: 0px 4px 5px #171D20;
align-items: center;
padding-top: 20px;
padding-bottom: 20px;
background: rgba(41, 41, 41, .6);
margin-top: 0px;
}
.logo {
align-self: center;
padding: 15px;
}
.icon {
align-self: center;
justify-content: center;
margin: 15px;
}
.input_box {
border: none;
outline: none;
background: none;
padding-right: 15px;
padding-left: 15px;
width: 100%;
color: #ffa31a;
font-size: 22px;
border-radius: 0px 0px 0px 0px;
height: 70px;
text-align: center;
}
.button {
background: none;
border: none;
transition-duration: 0.6s;
position: fixed;
padding-left: 11px;
padding-right: 12px;
height: 70px;
opacity: 60%;
}
.engine {
background: none;
padding-left: 12px;
padding-right: 11px;
border: none;
transition-duration: 0.6s;
position: relative;
opacity: 60%;
}
form {
margin: 0px;
padding: 0px;
}
.search_box {
display: flex;
background: rgba(41, 41, 41, .6);
width: 600px;
padding: 0px;
margin-bottom: 40px;
border-radius: 3px 3px 3px 3px;
box-shadow: 0px 4px 5px #171D20;
align-self: center;
height: 70px;
}
.engine:hover {
filter: saturate(150%) brightness(120%) !important;
opacity: 100%;
}
.button:hover {
filter: saturate(150%) brightness(120%) !important;
opacity: 100%;
}
li {
font-size: 16px;
list-style-type: none;
padding: 5px
}
a:link {
text-decoration: none;
color: #F8BE4D;
}
a:visited {
color: #F8BE4D;
}
a:hover {
color: #B5CCE8;
}
#credit {
position: fixed;
right: 0;
bottom: 0;
font-family: iosevka-light;
}
#gSearch {
width: 100%;
}
@media screen and (max-width: 600px) {
.container {
display: block;
}
.bookmark {
flex-direction: column;
}
.category {
width: 90%;
align-self: center;
}
.search_box {
width: 90%;
}
.input_box {
width: 100%;
padding: 0px;
}
.engine {
padding: 0px 5px;
}
}

View File

@ -0,0 +1,305 @@
/**
* VoidPage - Main application controller
* Coordinates configuration loading, styling, and page initialization
*/
class VoidPage {
constructor() {
this.config = null;
this.configLoader = null;
this.styleParser = null;
this.isInitialized = false;
}
/**
* Initialize the VoidPage application
*/
async init() {
try {
console.log('Initializing VoidPage...');
// Import required modules
await this.loadModules();
// Load configuration
await this.loadConfiguration();
// Apply styles based on configuration
await this.applyStyles();
// Initialize page content
this.initializePageContent();
// Set up event listeners
this.setupEventListeners();
this.isInitialized = true;
console.log('VoidPage initialized successfully');
} catch (error) {
console.error('Failed to initialize VoidPage:', error);
this.handleInitializationError(error);
}
}
/**
* Load required modules dynamically
*/
async loadModules() {
try {
// Import configLoader module
const configModule = await import('./configLoader.js');
this.configLoader = configModule.configLoader;
// Import styleParser module
const styleModule = await import('./styleParser.js');
this.styleParser = styleModule.styleParser;
console.log('Modules loaded successfully');
} catch (error) {
console.error('Failed to load modules:', error);
throw new Error('Module loading failed');
}
}
/**
* Load and parse configuration
*/
async loadConfiguration() {
try {
this.config = await this.configLoader.loadConfig();
// Make config globally available for other scripts
window.VOID_CONFIG = this.config;
console.log('Configuration loaded:', this.config);
} catch (error) {
console.error('Failed to load configuration:', error);
throw new Error('Configuration loading failed');
}
}
/**
* Apply styles based on configuration
*/
async applyStyles() {
try {
if (!this.config) {
throw new Error('Configuration not loaded');
}
const success = this.styleParser.applyConfigStyles(this.config);
if (!success) {
throw new Error('Style application failed');
}
console.log('Styles applied successfully');
} catch (error) {
console.error('Failed to apply styles:', error);
// Don't throw - allow page to continue with default styles
}
}
/**
* Initialize page content based on configuration
*/
initializePageContent() {
try {
const settings = this.config.settings;
const cards = this.config.cards;
// Set page title
document.title = settings.title;
// Create or update page header
this.createPageHeader(settings);
// Generate cards
this.generateCards(cards);
// Set up search functionality
this.setupSearch(settings.searchEngine);
console.log('Page content initialized');
} catch (error) {
console.error('Failed to initialize page content:', error);
}
}
/**
* Create page header
*/
createPageHeader(settings) {
let header = document.querySelector('.header');
if (!header) {
header = document.createElement('header');
header.className = 'header';
document.body.insertBefore(header, document.body.firstChild);
}
header.innerHTML = `
<h1>${settings.title}</h1>
<div class="search-container">
<input type="text" id="search-input" placeholder="Search ${settings.searchEngine.name}...">
<button id="search-button">Search</button>
</div>
`;
}
/**
* Generate cards from configuration
*/
generateCards(cards) {
let container = document.querySelector('.cards-container');
if (!container) {
container = document.createElement('div');
container.className = 'cards-container';
document.body.appendChild(container);
}
// Clear existing cards
container.innerHTML = '';
cards.forEach(cardData => {
const cardElement = this.createCardElement(cardData);
container.appendChild(cardElement);
});
}
/**
* Create individual card element
*/
createCardElement(cardData) {
const card = document.createElement('div');
card.className = 'card';
const openInNewTab = this.config.settings.openLinksInNewTab;
const targetAttr = openInNewTab ? 'target="_blank" rel="noopener noreferrer"' : '';
card.innerHTML = `
<div class="card-header">
<div class="card-icon" style="background-image: url('${cardData.iconPath}')"></div>
<h3 class="card-title">${cardData.title}</h3>
</div>
<div class="card-links">
${cardData.links.map(link =>
`<a href="${link.url}" class="card-link" ${targetAttr}>${link.name}</a>`
).join('')}
</div>
`;
return card;
}
/**
* Set up search functionality
*/
setupSearch(searchEngine) {
const searchInput = document.getElementById('search-input');
const searchButton = document.getElementById('search-button');
if (!searchInput || !searchButton) return;
const performSearch = () => {
const query = searchInput.value.trim();
if (query) {
const searchUrl = searchEngine.url + encodeURIComponent(query);
if (this.config.settings.openLinksInNewTab) {
window.open(searchUrl, '_blank', 'noopener,noreferrer');
} else {
window.location.href = searchUrl;
}
}
};
searchButton.addEventListener('click', performSearch);
searchInput.addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
performSearch();
}
});
}
/**
* Set up additional event listeners
*/
setupEventListeners() {
// Handle configuration updates
window.addEventListener('configUpdated', (event) => {
this.handleConfigUpdate(event.detail);
});
// Handle window resize for responsive behavior
window.addEventListener('resize', () => {
this.handleResize();
});
}
/**
* Handle configuration updates
*/
handleConfigUpdate(newConfig) {
console.log('Configuration updated, refreshing page...');
this.config = newConfig;
window.VOID_CONFIG = newConfig;
// Reapply styles and content
this.applyStyles();
this.initializePageContent();
}
/**
* Handle window resize
*/
handleResize() {
// Add any responsive behavior here
console.log('Window resized');
}
/**
* Handle initialization errors
*/
handleInitializationError(error) {
console.error('VoidPage initialization failed:', error);
// Create error message for user
const errorDiv = document.createElement('div');
errorDiv.className = 'error-message';
errorDiv.innerHTML = `
<h2>Loading Error</h2>
<p>Failed to load the page configuration. Please check the console for details.</p>
<button onclick="location.reload()">Retry</button>
`;
document.body.appendChild(errorDiv);
}
/**
* Get current configuration
*/
getConfig() {
return this.config;
}
/**
* Check if page is initialized
*/
isReady() {
return this.isInitialized;
}
}
// Create global VoidPage instance
const voidPage = new VoidPage();
// Initialize when DOM is ready
document.addEventListener('DOMContentLoaded', () => {
voidPage.init();
});
// Make VoidPage globally available
window.VoidPage = VoidPage;
window.voidPage = voidPage;
// Export for module usage
// export { VoidPage, voidPage };

View File

@ -0,0 +1,94 @@
/**
* Configuration loader utility
* Loads and parses the config.json file from the server
*/
class ConfigLoader {
constructor() {
this.config = null;
this.configPath = '/src/config.json';
}
/**
* Load configuration from the server
* @returns {Promise<Object>} The parsed configuration object
*/
async loadConfig() {
try {
const response = await fetch(this.configPath);
if (!response.ok) {
throw new Error(`Failed to load config: ${response.status} ${response.statusText}`);
}
const configText = await response.text();
this.config = JSON.parse(configText);
console.log('Configuration loaded successfully:', this.config);
return this.config;
} catch (error) {
console.error('Error loading configuration:', error);
throw error;
}
}
/**
* Get the loaded configuration
* @returns {Object|null} The configuration object or null if not loaded
*/
getConfig() {
return this.config;
}
/**
* Get a specific configuration value by key
* @param {string} key - The configuration key
* @param {*} defaultValue - Default value if key is not found
* @returns {*} The configuration value
*/
get(key, defaultValue = null) {
if (!this.config) {
console.warn('Configuration not loaded yet. Call loadConfig() first.');
return defaultValue;
}
return this.config.hasOwnProperty(key) ? this.config[key] : defaultValue;
}
/**
* Check if configuration is loaded
* @returns {boolean} True if config is loaded
*/
isLoaded() {
return this.config !== null;
}
}
// Create a global instance for easy access
const configLoader = new ConfigLoader();
// Alternative function-based approach for simpler usage
async function loadConfiguration() {
try {
const response = await fetch('/src/config.json');
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const config = await response.json();
console.log('Configuration loaded:', config);
return config;
} catch (error) {
console.error('Failed to load configuration:', error);
throw error;
}
}
// Export for module usage (if using ES6 modules)
// export { ConfigLoader, configLoader, loadConfiguration };
// For immediate use in browser without modules
window.ConfigLoader = ConfigLoader;
window.configLoader = configLoader;
window.loadConfiguration = loadConfiguration;

View File

@ -0,0 +1,9 @@
document.addEventListener("DOMContentLoaded", () => {
fetch('/assets/backgrounds/bgs.json')
.then(response => response.json())
.then(images => {
const randomImage = images[Math.floor(Math.random() * images.length)];
document.body.style.backgroundImage = `url(${randomImage})`;
})
.catch(error => console.error('Error fetching image list:', error));
});

View File

@ -0,0 +1,270 @@
/**
* Style Parser and Applicator
* Builds and applies CSS stylesheets based on configuration values
*/
class StyleParser {
constructor() {
this.dynamicStyleElement = null;
this.cssVariablesApplied = false;
}
/**
* Build CSS stylesheet text from configuration
* @param {Object} config - The parsed configuration object
* @returns {string} Complete CSS stylesheet text
*/
buildStylesheet(config) {
const { settings, cards } = config;
let css = `
/* Dynamic stylesheet generated from config.json */
/* Root CSS Variables */
:root {
--bg-image: url('${settings.backgroundImgPath}');
--header-bg-color: ${settings.headerBgColor};
--header-outline-color: ${settings.headerOutlineColor};
--header-outline-thickness: ${settings.headerOutlineThickness}px;
--card-bg-color: ${settings.cardBgColor};
--corner-radius: ${settings.cornerRadius}px;
}
/* Body and Background */
body {
background-image: var(--bg-image);
background-size: cover;
background-position: center;
background-repeat: no-repeat;
background-attachment: fixed;
}
/* Header Styles */
.header,
header {
background-color: var(--header-bg-color);
border: var(--header-outline-thickness) solid var(--header-outline-color);
border-radius: var(--corner-radius);
}
/* Card Styles */
.card {
background-color: var(--card-bg-color);
border-radius: var(--corner-radius);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
}
/* Card Container */
.cards-container {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 20px;
padding: 20px;
}
/* Link Target Behavior */
`;
// Add link target behavior based on config
if (settings.openLinksInNewTab) {
css += `
.card a {
target: _blank;
}
`;
}
// Add card-specific styles if needed
cards.forEach((card, index) => {
if (card.iconPath) {
css += `
.card:nth-child(${index + 1}) .card-icon {
background-image: url('${card.iconPath}');
background-size: contain;
background-repeat: no-repeat;
background-position: center;
}
`;
}
});
css += `
/* Card Icons */
.card-icon {
width: 32px;
height: 32px;
display: inline-block;
margin-right: 10px;
}
/* Responsive Design */
@media (max-width: 768px) {
.cards-container {
grid-template-columns: 1fr;
padding: 10px;
}
.card {
margin-bottom: 15px;
}
}
`;
return css;
}
/**
* Apply CSS variables to document root
* @param {Object} settings - Configuration settings object
*/
applyCSSVariables(settings) {
const root = document.documentElement;
root.style.setProperty('--bg-image', `url('${settings.backgroundImgPath}')`);
root.style.setProperty('--header-bg-color', settings.headerBgColor);
root.style.setProperty('--header-outline-color', settings.headerOutlineColor);
root.style.setProperty('--header-outline-thickness', `${settings.headerOutlineThickness}px`);
root.style.setProperty('--card-bg-color', settings.cardBgColor);
root.style.setProperty('--corner-radius', `${settings.cornerRadius}px`);
this.cssVariablesApplied = true;
console.log('CSS variables applied to document root');
}
/**
* Create and inject dynamic stylesheet into document head
* @param {string} cssText - CSS stylesheet text
* @returns {HTMLStyleElement} The created style element
*/
injectStylesheet(cssText) {
// Remove existing dynamic stylesheet if present
this.removeDynamicStylesheet();
// Create new style element
this.dynamicStyleElement = document.createElement('style');
this.dynamicStyleElement.type = 'text/css';
this.dynamicStyleElement.id = 'dynamic-config-styles';
this.dynamicStyleElement.textContent = cssText;
// Inject into document head
document.head.appendChild(this.dynamicStyleElement);
console.log('Dynamic stylesheet injected');
return this.dynamicStyleElement;
}
/**
* Remove the dynamic stylesheet from document
*/
removeDynamicStylesheet() {
if (this.dynamicStyleElement) {
this.dynamicStyleElement.remove();
this.dynamicStyleElement = null;
console.log('Dynamic stylesheet removed');
}
}
/**
* Apply link target behavior to existing links
* @param {boolean} openInNewTab - Whether to open links in new tab
*/
applyLinkTargetBehavior(openInNewTab) {
const links = document.querySelectorAll('.card a, .card-link');
links.forEach(link => {
if (openInNewTab) {
link.setAttribute('target', '_blank');
link.setAttribute('rel', 'noopener noreferrer');
} else {
link.removeAttribute('target');
link.removeAttribute('rel');
}
});
console.log(`Link target behavior applied: ${openInNewTab ? 'new tab' : 'same tab'}`);
}
/**
* Complete style application from configuration
* @param {Object} config - The complete configuration object
*/
applyConfigStyles(config) {
try {
// Build and inject stylesheet
const cssText = this.buildStylesheet(config);
this.injectStylesheet(cssText);
// Apply CSS variables
this.applyCSSVariables(config.settings);
// Apply link behavior
this.applyLinkTargetBehavior(config.settings.openLinksInNewTab);
console.log('All configuration styles applied successfully');
return true;
} catch (error) {
console.error('Error applying configuration styles:', error);
return false;
}
}
/**
* Update specific style property
* @param {string} property - CSS property name
* @param {string} value - CSS property value
*/
updateStyleProperty(property, value) {
const root = document.documentElement;
const cssVarName = `--${property.replace(/([A-Z])/g, '-$1').toLowerCase()}`;
root.style.setProperty(cssVarName, value);
console.log(`Updated ${cssVarName}: ${value}`);
}
/**
* Get current dynamic stylesheet text
* @returns {string|null} Current stylesheet text or null if not applied
*/
getCurrentStylesheet() {
return this.dynamicStyleElement ? this.dynamicStyleElement.textContent : null;
}
/**
* Check if styles are currently applied
* @returns {boolean} True if dynamic styles are active
*/
isStylesApplied() {
return this.dynamicStyleElement !== null && this.cssVariablesApplied;
}
}
// Create global instance
const styleParser = new StyleParser();
// Utility functions for external access
function applyConfigurationStyles(config) {
return styleParser.applyConfigStyles(config);
}
function buildStylesheetFromConfig(config) {
return styleParser.buildStylesheet(config);
}
function updateStyleProperty(property, value) {
return styleParser.updateStyleProperty(property, value);
}
function removeConfigurationStyles() {
return styleParser.removeDynamicStylesheet();
}
// Export for module usage
// export { StyleParser, styleParser, applyConfigurationStyles, buildStylesheetFromConfig };
// Global access
window.StyleParser = StyleParser;
window.styleParser = styleParser;
window.applyConfigurationStyles = applyConfigurationStyles;
window.buildStylesheetFromConfig = buildStylesheetFromConfig;
window.updateStyleProperty = updateStyleProperty;
window.removeConfigurationStyles = removeConfigurationStyles;

View File

@ -1,16 +1,49 @@
<!DOCTYPE html> <!DOCTYPE html>
<html> <html lang="en">
<head> <head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>VoidPage</title> <title>VoidPage</title>
<link rel="stylesheet" type="text/css" href="assets/main.css">
<script src="https://kit.fontawesome.com/a81a0c775a.js" crossorigin="anonymous"></script>
<!-- Background randomizer -->
<script defer src="assets/scripts/randomBackgrounds.js"></script>
<link rel="icon" type="image/x-icon" href="assets/images/favicon.ico">
</head> </head>
<body> <body>
<!-- Logo Container --> <div class="container">
<div id="logo-container"> <div class="head">
<!-- Above-Search Logo -->
<div class="logo"><img src="assets/images/fox.svg" /></div>
<div class="search_box">
<!-- Google Search -->
<div id="gSearch" style="display:block;">
<form id="textField" class="google" action="https://google.com/search" method="get">
<input class="input_box" type="text" name="q" placeholder="Search" autofocus>
</form>
</div>
</div>
</div>
<div class="bookmark">
<div class="category">
<p class="title">Category One</p>
<div class="bookmarks">
<li><a class="bm" href="https://google.com">Google</a></li>
</div>
</div>
<div class="category">
<p class="title">Category Two</p>
<div class="bookmarks">
<li><a class="bm" href="https://steampowered.com">Steam</a></li>
</div>
</div>
<div class="category">
<p class="title">Category Three</p>
<div class="bookmarks">
<li><a class="bm" href="https://mail.google.com">GMail</a></li>
</div>
</div>
</div>
</div> </div>
<!-- Search Bar -->
<!-- Categories -->
</body> </body>
</html> </html>