mirror of
https://github.com/CNCKitchen/stlTexturizer.git
synced 2026-04-07 22:11:32 +00:00
feat: add store CTA and dimension annotations, update safety cap and translations
This commit is contained in:
+17
-14
@@ -72,6 +72,9 @@
|
|||||||
</div>
|
</div>
|
||||||
<canvas id="viewport"></canvas>
|
<canvas id="viewport"></canvas>
|
||||||
<div id="brush-cursor"></div>
|
<div id="brush-cursor"></div>
|
||||||
|
<div id="store-cta-wrapper">
|
||||||
|
<a id="store-cta" href="https://geni.us/CNCStoreTexture" target="_blank" rel="noopener noreferrer" data-i18n="cta.store">🛒 Enjoying this free tool? Support us — Shop at CNCKitchen.STORE!</a>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="viewport-footer">
|
<div id="viewport-footer">
|
||||||
<span id="mesh-info" class="mesh-info"></span>
|
<span id="mesh-info" class="mesh-info"></span>
|
||||||
@@ -129,6 +132,16 @@
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<!-- Displacement -->
|
||||||
|
<section class="panel-section">
|
||||||
|
<h2 data-i18n="sections.displacement">Displacement</h2>
|
||||||
|
<div class="form-row slider-row">
|
||||||
|
<label for="amplitude" data-i18n="labels.amplitude">Amplitude</label>
|
||||||
|
<input type="range" id="amplitude" min="-2" max="2" step="0.01" value="0.5" />
|
||||||
|
<input type="number" class="val" id="amplitude-val" value="0.5" min="-100" max="100" step="0.01" />
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
<!-- Transform -->
|
<!-- Transform -->
|
||||||
<section class="panel-section">
|
<section class="panel-section">
|
||||||
<h2 data-i18n="sections.transform">Transform</h2>
|
<h2 data-i18n="sections.transform">Transform</h2>
|
||||||
@@ -171,16 +184,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- Displacement -->
|
|
||||||
<section class="panel-section">
|
|
||||||
<h2 data-i18n="sections.displacement">Displacement</h2>
|
|
||||||
<div class="form-row slider-row">
|
|
||||||
<label for="amplitude" data-i18n="labels.amplitude">Amplitude</label>
|
|
||||||
<input type="range" id="amplitude" min="-2" max="2" step="0.01" value="0.5" />
|
|
||||||
<input type="number" class="val" id="amplitude-val" value="0.5" min="-100" max="100" step="0.01" />
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- Surface Mask -->
|
<!-- Surface Mask -->
|
||||||
<section class="panel-section">
|
<section class="panel-section">
|
||||||
<h2 data-i18n="sections.surfaceMask" data-i18n-title="tooltips.surfaceMask" title="0° = no masking. Surfaces within this angle of horizontal will not be textured.">Surface Mask ⓘ</h2>
|
<h2 data-i18n="sections.surfaceMask" data-i18n-title="tooltips.surfaceMask" title="0° = no masking. Surfaces within this angle of horizontal will not be textured.">Surface Mask ⓘ</h2>
|
||||||
@@ -280,16 +283,16 @@
|
|||||||
<h2 data-i18n="sections.export" data-i18n-title="tooltips.export" title="Smaller edge length = finer displacement detail. Output is then decimated to the triangle limit.">Export ⓘ</h2>
|
<h2 data-i18n="sections.export" data-i18n-title="tooltips.export" title="Smaller edge length = finer displacement detail. Output is then decimated to the triangle limit.">Export ⓘ</h2>
|
||||||
<div class="form-row slider-row">
|
<div class="form-row slider-row">
|
||||||
<label for="refine-length" data-i18n="labels.resolution" data-i18n-title="tooltips.resolution" title="Edges longer than this value will be split during export">Resolution</label>
|
<label for="refine-length" data-i18n="labels.resolution" data-i18n-title="tooltips.resolution" title="Edges longer than this value will be split during export">Resolution</label>
|
||||||
<input type="range" id="refine-length" min="0.1" max="5" step="0.1" value="1" />
|
<input type="range" id="refine-length" min="0.05" max="5" step="0.05" value="1" />
|
||||||
<input type="number" class="val" id="refine-length-val" value="1" min="0.1" max="5" step="0.1" />
|
<input type="number" class="val" id="refine-length-val" value="1" min="0.01" max="100" step="0.01" />
|
||||||
</div>
|
</div>
|
||||||
<div class="form-row slider-row">
|
<div class="form-row slider-row">
|
||||||
<label for="max-triangles" data-i18n="labels.outputTriangles" data-i18n-title="tooltips.outputTriangles" title="Mesh is fully subdivided first, then decimated down to this count">Output Triangles</label>
|
<label for="max-triangles" data-i18n="labels.outputTriangles" data-i18n-title="tooltips.outputTriangles" title="Mesh is fully subdivided first, then decimated down to this count">Output Triangles</label>
|
||||||
<input type="range" id="max-triangles" min="10000" max="5000000" step="10000" value="1000000" />
|
<input type="range" id="max-triangles" min="10000" max="10000000" step="10000" value="1000000" />
|
||||||
<span class="val" id="max-triangles-val">1.0 M</span>
|
<span class="val" id="max-triangles-val">1.0 M</span>
|
||||||
</div>
|
</div>
|
||||||
<div id="tri-limit-warning" class="tri-limit-warning hidden" data-i18n="warnings.safetyCapHit">
|
<div id="tri-limit-warning" class="tri-limit-warning hidden" data-i18n="warnings.safetyCapHit">
|
||||||
⚠ 5M-triangle safety cap hit during subdivision — result may still be coarser than requested edge length.
|
⚠ 10M-triangle safety cap hit during subdivision — result may still be coarser than requested edge length.
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="export-progress" class="export-progress hidden">
|
<div id="export-progress" class="export-progress hidden">
|
||||||
|
|||||||
+12
-4
@@ -95,7 +95,7 @@ export const TRANSLATIONS = {
|
|||||||
'tooltips.resolution': 'Edges longer than this value will be split during export',
|
'tooltips.resolution': 'Edges longer than this value will be split during export',
|
||||||
'labels.outputTriangles': 'Output Triangles',
|
'labels.outputTriangles': 'Output Triangles',
|
||||||
'tooltips.outputTriangles': 'Mesh is fully subdivided first, then decimated down to this count',
|
'tooltips.outputTriangles': 'Mesh is fully subdivided first, then decimated down to this count',
|
||||||
'warnings.safetyCapHit': '\u26a0 5M-triangle safety cap hit during subdivision \u2014 result may still be coarser than requested edge length.',
|
'warnings.safetyCapHit': '\u26a0 10M-triangle safety cap hit during subdivision \u2014 result may still be coarser than requested edge length.',
|
||||||
'ui.exportStl': 'Export STL',
|
'ui.exportStl': 'Export STL',
|
||||||
|
|
||||||
// Export progress stages
|
// Export progress stages
|
||||||
@@ -115,6 +115,10 @@ export const TRANSLATIONS = {
|
|||||||
'sponsor.dontShow': "Don\u2019t show this again",
|
'sponsor.dontShow': "Don\u2019t show this again",
|
||||||
'sponsor.closeAndContinue':'Close & Continue',
|
'sponsor.closeAndContinue':'Close & Continue',
|
||||||
|
|
||||||
|
// Store CTA
|
||||||
|
'cta.store': '\uD83D\uDED2 Enjoying this free tool? Support us & shop at CNCKitchen.STORE!',
|
||||||
|
'cta.storeDismiss': 'Dismiss',
|
||||||
|
|
||||||
// Alerts
|
// Alerts
|
||||||
'alerts.loadFailed': 'Could not load STL: {msg}',
|
'alerts.loadFailed': 'Could not load STL: {msg}',
|
||||||
'alerts.exportFailed': 'Export failed: {msg}',
|
'alerts.exportFailed': 'Export failed: {msg}',
|
||||||
@@ -132,7 +136,7 @@ export const TRANSLATIONS = {
|
|||||||
|
|
||||||
// Viewport footer
|
// Viewport footer
|
||||||
'ui.wireframe': 'Drahtgitter',
|
'ui.wireframe': 'Drahtgitter',
|
||||||
'ui.controlsHint': 'Links ziehen: Drehen \u00a0·\u00a0 Rechts ziehen: Verschieben \u00a0·\u00a0 Scrollen: Zoomen',
|
'ui.controlsHint': 'Linke Maustaste: Drehen \u00a0·\u00a0 Rechte Maustaste: Verschieben \u00a0·\u00a0 Mausrad: Zoomen',
|
||||||
'ui.meshInfo': '{n} Dreiecke · {mb} MB',
|
'ui.meshInfo': '{n} Dreiecke · {mb} MB',
|
||||||
|
|
||||||
// Load STL button
|
// Load STL button
|
||||||
@@ -165,7 +169,7 @@ export const TRANSLATIONS = {
|
|||||||
'tooltips.proportionalScalingAria': 'Proportionale Skalierung (U = V)',
|
'tooltips.proportionalScalingAria': 'Proportionale Skalierung (U = V)',
|
||||||
|
|
||||||
// Displacement section
|
// Displacement section
|
||||||
'sections.displacement': 'Verschiebung',
|
'sections.displacement': 'Texturtiefe',
|
||||||
'labels.amplitude': 'Amplitude',
|
'labels.amplitude': 'Amplitude',
|
||||||
|
|
||||||
// Surface mask section
|
// Surface mask section
|
||||||
@@ -213,7 +217,7 @@ export const TRANSLATIONS = {
|
|||||||
'tooltips.resolution': 'Kanten l\u00e4nger als dieser Wert werden beim Export unterteilt',
|
'tooltips.resolution': 'Kanten l\u00e4nger als dieser Wert werden beim Export unterteilt',
|
||||||
'labels.outputTriangles': 'Max Dreiecke',
|
'labels.outputTriangles': 'Max Dreiecke',
|
||||||
'tooltips.outputTriangles': 'Das Netz wird zuerst vollst\u00e4ndig unterteilt, dann auf diese Anzahl dezimiert',
|
'tooltips.outputTriangles': 'Das Netz wird zuerst vollst\u00e4ndig unterteilt, dann auf diese Anzahl dezimiert',
|
||||||
'warnings.safetyCapHit': '\u26a0 5-Mio.-Dreiecke-Sicherheitsgrenze bei der Unterteilung erreicht \u2014 Ergebnis kann gr\u00f6ber als gew\u00fcnschte Kantenl\u00e4nge sein.',
|
'warnings.safetyCapHit': '\u26a0 10-Mio.-Dreiecke-Sicherheitsgrenze bei der Unterteilung erreicht \u2014 Ergebnis kann gr\u00f6ber als gew\u00fcnschte Kantenl\u00e4nge sein.',
|
||||||
'ui.exportStl': 'STL exportieren',
|
'ui.exportStl': 'STL exportieren',
|
||||||
|
|
||||||
// Export progress stages
|
// Export progress stages
|
||||||
@@ -233,6 +237,10 @@ export const TRANSLATIONS = {
|
|||||||
'sponsor.dontShow': 'Nicht mehr anzeigen',
|
'sponsor.dontShow': 'Nicht mehr anzeigen',
|
||||||
'sponsor.closeAndContinue':'Schlie\u00dfen & Weiter',
|
'sponsor.closeAndContinue':'Schlie\u00dfen & Weiter',
|
||||||
|
|
||||||
|
// Store CTA
|
||||||
|
'cta.store': '\uD83D\uDED2 Dieses Tool ist kostenlos - schau deshalb mal bei CNCKitchen.STORE vorbei!',
|
||||||
|
'cta.storeDismiss': 'Ausblenden',
|
||||||
|
|
||||||
// Alerts
|
// Alerts
|
||||||
'alerts.loadFailed': 'STL konnte nicht geladen werden: {msg}',
|
'alerts.loadFailed': 'STL konnte nicht geladen werden: {msg}',
|
||||||
'alerts.exportFailed': 'Export fehlgeschlagen: {msg}',
|
'alerts.exportFailed': 'Export fehlgeschlagen: {msg}',
|
||||||
|
|||||||
+5
-5
@@ -300,14 +300,14 @@ function wireEvents() {
|
|||||||
linkSlider(offsetVSlider, offsetVVal, v => { settings.offsetV = v; return v.toFixed(2); });
|
linkSlider(offsetVSlider, offsetVVal, v => { settings.offsetV = v; return v.toFixed(2); });
|
||||||
linkSlider(rotationSlider, rotationVal, v => { settings.rotation = v; return Math.round(v); });
|
linkSlider(rotationSlider, rotationVal, v => { settings.rotation = v; return Math.round(v); });
|
||||||
linkSlider(amplitudeSlider, amplitudeVal, v => { settings.amplitude = v; return v.toFixed(2); });
|
linkSlider(amplitudeSlider, amplitudeVal, v => { settings.amplitude = v; return v.toFixed(2); });
|
||||||
linkSlider(refineLenSlider, refineLenVal, v => { settings.refineLength = v; return v.toFixed(1); }, false);
|
linkSlider(refineLenSlider, refineLenVal, v => { settings.refineLength = v; return v.toFixed(2); }, false);
|
||||||
linkSlider(maxTriSlider, maxTriVal, v => { settings.maxTriangles = v; return formatM(v); }, false);
|
linkSlider(maxTriSlider, maxTriVal, v => { settings.maxTriangles = v; return formatM(v); }, false);
|
||||||
linkSlider(bottomAngleLimitSlider, bottomAngleLimitVal, v => { settings.bottomAngleLimit = v; return v; });
|
linkSlider(bottomAngleLimitSlider, bottomAngleLimitVal, v => { settings.bottomAngleLimit = v; return v; });
|
||||||
linkSlider(topAngleLimitSlider, topAngleLimitVal, v => { settings.topAngleLimit = v; return v; });
|
linkSlider(topAngleLimitSlider, topAngleLimitVal, v => { settings.topAngleLimit = v; return v; });
|
||||||
|
|
||||||
// ── Export ──
|
// ── Export ──
|
||||||
exportBtn.addEventListener('click', () => {
|
exportBtn.addEventListener('click', () => {
|
||||||
if (localStorage.getItem('stlt-no-sponsor') === '1') {
|
if (sessionStorage.getItem('stlt-no-sponsor') === '1') {
|
||||||
handleExport();
|
handleExport();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -318,7 +318,7 @@ function wireEvents() {
|
|||||||
|
|
||||||
const dismiss = () => {
|
const dismiss = () => {
|
||||||
if (document.getElementById('sponsor-dont-show').checked) {
|
if (document.getElementById('sponsor-dont-show').checked) {
|
||||||
localStorage.setItem('stlt-no-sponsor', '1');
|
sessionStorage.setItem('stlt-no-sponsor', '1');
|
||||||
}
|
}
|
||||||
overlay.classList.add('hidden');
|
overlay.classList.add('hidden');
|
||||||
handleExport();
|
handleExport();
|
||||||
@@ -722,9 +722,9 @@ async function handleSTL(file) {
|
|||||||
settings.offsetV = 0; resetVal(offsetVSlider, offsetVVal, 0);
|
settings.offsetV = 0; resetVal(offsetVSlider, offsetVVal, 0);
|
||||||
triLimitWarning.classList.add('hidden');
|
triLimitWarning.classList.add('hidden');
|
||||||
|
|
||||||
// Default edge length = 1/100 of the largest bounding box dimension
|
// Default edge length = 1/200 of the largest bounding box dimension
|
||||||
const maxDim = Math.max(bounds.size.x, bounds.size.y, bounds.size.z);
|
const maxDim = Math.max(bounds.size.x, bounds.size.y, bounds.size.z);
|
||||||
const defaultEdge = Math.max(0.1, Math.min(5.0, +(maxDim / 100).toFixed(2)));
|
const defaultEdge = Math.max(0.05, Math.min(5.0, +(maxDim / 200).toFixed(2)));
|
||||||
settings.refineLength = defaultEdge;
|
settings.refineLength = defaultEdge;
|
||||||
refineLenSlider.value = defaultEdge;
|
refineLenSlider.value = defaultEdge;
|
||||||
refineLenVal.value = defaultEdge;
|
refineLenVal.value = defaultEdge;
|
||||||
|
|||||||
+1
-1
@@ -15,7 +15,7 @@
|
|||||||
import * as THREE from 'three';
|
import * as THREE from 'three';
|
||||||
|
|
||||||
const QUANTISE = 1e4;
|
const QUANTISE = 1e4;
|
||||||
const SAFETY_CAP = 5_000_000; // absolute OOM guard
|
const SAFETY_CAP = 10_000_000; // absolute OOM guard
|
||||||
|
|
||||||
// ── Public entry point ───────────────────────────────────────────────────────
|
// ── Public entry point ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { LineMaterial } from 'three/addons/lines/LineMaterial.js';
|
|||||||
let renderer, camera, scene, controls, meshGroup, ambientLight, dirLight1, dirLight2, grid;
|
let renderer, camera, scene, controls, meshGroup, ambientLight, dirLight1, dirLight2, grid;
|
||||||
let currentMesh = null;
|
let currentMesh = null;
|
||||||
let axesGroup = null;
|
let axesGroup = null;
|
||||||
|
let dimensionGroup = null;
|
||||||
let wireframeLines = null; // LineSegments overlay, or null when hidden
|
let wireframeLines = null; // LineSegments overlay, or null when hidden
|
||||||
let wireframeVisible = false;
|
let wireframeVisible = false;
|
||||||
let exclusionMesh = null; // flat orange overlay for user-excluded faces
|
let exclusionMesh = null; // flat orange overlay for user-excluded faces
|
||||||
@@ -63,6 +64,81 @@ function buildAxesIndicator(size) {
|
|||||||
return group;
|
return group;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Create a canvas-texture sprite label for a dimension annotation.
|
||||||
|
// Flat ground-plane label — no billboard, no background, lies directly on the bed.
|
||||||
|
function buildDimensionLabel(text, hex, worldW, worldH) {
|
||||||
|
const c = document.createElement('canvas');
|
||||||
|
c.width = 256;
|
||||||
|
c.height = 64;
|
||||||
|
const ctx = c.getContext('2d');
|
||||||
|
ctx.clearRect(0, 0, 256, 64);
|
||||||
|
ctx.fillStyle = `#${hex.toString(16).padStart(6, '0')}`;
|
||||||
|
ctx.font = 'bold 36px Arial';
|
||||||
|
ctx.textAlign = 'center';
|
||||||
|
ctx.textBaseline = 'middle';
|
||||||
|
ctx.fillText(text, 128, 32);
|
||||||
|
const mesh = new THREE.Mesh(
|
||||||
|
new THREE.PlaneGeometry(worldW, worldH),
|
||||||
|
new THREE.MeshBasicMaterial({ map: new THREE.CanvasTexture(c), transparent: true, depthTest: false, side: THREE.DoubleSide }),
|
||||||
|
);
|
||||||
|
mesh.renderOrder = 998;
|
||||||
|
return mesh;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build X/Y dimension-line annotations lying flat on the ground plane.
|
||||||
|
function buildDimensions(box, groundZ, scale) {
|
||||||
|
const group = new THREE.Group();
|
||||||
|
const fmt = v => v.toFixed(2);
|
||||||
|
const pad = scale * 0.18;
|
||||||
|
const tick = scale * 0.08;
|
||||||
|
const lblW = scale * 0.50;
|
||||||
|
const lblH = scale * 0.12;
|
||||||
|
const zOff = 0.02; // tiny lift to avoid z-fighting with the grid
|
||||||
|
|
||||||
|
const addLine = (pts, hex) => {
|
||||||
|
const line = new THREE.Line(
|
||||||
|
new THREE.BufferGeometry().setFromPoints(pts),
|
||||||
|
new THREE.LineBasicMaterial({ color: hex, depthTest: false, transparent: true, opacity: 0.75 }),
|
||||||
|
);
|
||||||
|
line.renderOrder = 997;
|
||||||
|
group.add(line);
|
||||||
|
};
|
||||||
|
|
||||||
|
const addTick = (centre, dir, hex) => {
|
||||||
|
addLine([
|
||||||
|
centre.clone().addScaledVector(dir, -tick * 0.5),
|
||||||
|
centre.clone().addScaledVector(dir, tick * 0.5),
|
||||||
|
], hex);
|
||||||
|
};
|
||||||
|
|
||||||
|
// X dimension — line along the front edge of the model
|
||||||
|
{
|
||||||
|
const hex = 0xff3333;
|
||||||
|
const y = box.min.y - pad;
|
||||||
|
addLine([new THREE.Vector3(box.min.x, y, groundZ), new THREE.Vector3(box.max.x, y, groundZ)], hex);
|
||||||
|
addTick(new THREE.Vector3(box.min.x, y, groundZ), new THREE.Vector3(0, 1, 0), hex);
|
||||||
|
addTick(new THREE.Vector3(box.max.x, y, groundZ), new THREE.Vector3(0, 1, 0), hex);
|
||||||
|
const lbl = buildDimensionLabel(`X: ${fmt(box.max.x - box.min.x)}`, hex, lblW, lblH);
|
||||||
|
lbl.position.set((box.min.x + box.max.x) / 2, y - lblH * 0.7, groundZ + zOff);
|
||||||
|
group.add(lbl);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Y dimension — line along the right edge of the model
|
||||||
|
{
|
||||||
|
const hex = 0x33dd55;
|
||||||
|
const x = box.max.x + pad;
|
||||||
|
addLine([new THREE.Vector3(x, box.min.y, groundZ), new THREE.Vector3(x, box.max.y, groundZ)], hex);
|
||||||
|
addTick(new THREE.Vector3(x, box.min.y, groundZ), new THREE.Vector3(1, 0, 0), hex);
|
||||||
|
addTick(new THREE.Vector3(x, box.max.y, groundZ), new THREE.Vector3(1, 0, 0), hex);
|
||||||
|
const lbl = buildDimensionLabel(`Y: ${fmt(box.max.y - box.min.y)}`, hex, lblW, lblH);
|
||||||
|
lbl.position.set(x + lblH * 0.7, (box.min.y + box.max.y) / 2, groundZ + zOff);
|
||||||
|
lbl.rotation.z = Math.PI / 2;
|
||||||
|
group.add(lbl);
|
||||||
|
}
|
||||||
|
|
||||||
|
return group;
|
||||||
|
}
|
||||||
|
|
||||||
export function initViewer(canvas) {
|
export function initViewer(canvas) {
|
||||||
// Renderer
|
// Renderer
|
||||||
renderer = new THREE.WebGLRenderer({ canvas, antialias: true, alpha: false });
|
renderer = new THREE.WebGLRenderer({ canvas, antialias: true, alpha: false });
|
||||||
@@ -200,6 +276,11 @@ export function loadGeometry(geometry, material) {
|
|||||||
const axisPad = axisSize * 1.8;
|
const axisPad = axisSize * 1.8;
|
||||||
axesGroup.position.set(box.min.x - axisPad, box.min.y - axisPad, groundZ);
|
axesGroup.position.set(box.min.x - axisPad, box.min.y - axisPad, groundZ);
|
||||||
scene.add(axesGroup);
|
scene.add(axesGroup);
|
||||||
|
|
||||||
|
// Bounding-box dimension annotations on the ground plane
|
||||||
|
if (dimensionGroup) scene.remove(dimensionGroup);
|
||||||
|
dimensionGroup = buildDimensions(box, groundZ, sphere.radius);
|
||||||
|
scene.add(dimensionGroup);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -219,6 +219,41 @@ main {
|
|||||||
inset: 0;
|
inset: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#store-cta-wrapper {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 14px;
|
||||||
|
right: 14px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
z-index: 20;
|
||||||
|
pointer-events: none;
|
||||||
|
/* relative so the dismiss button can be positioned against the anchor */
|
||||||
|
}
|
||||||
|
#store-cta {
|
||||||
|
display: block;
|
||||||
|
position: relative;
|
||||||
|
padding: 10px 18px;
|
||||||
|
background: var(--accent);
|
||||||
|
color: #fff;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 700;
|
||||||
|
text-decoration: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: none;
|
||||||
|
pointer-events: all;
|
||||||
|
white-space: nowrap;
|
||||||
|
transition: background 0.15s, transform 0.15s;
|
||||||
|
letter-spacing: 0.01em;
|
||||||
|
}
|
||||||
|
#store-cta:hover {
|
||||||
|
background: var(--accent-hover);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
.store-cta-hidden {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
#viewport-footer {
|
#viewport-footer {
|
||||||
height: 28px;
|
height: 28px;
|
||||||
background: var(--surface);
|
background: var(--surface);
|
||||||
|
|||||||
Reference in New Issue
Block a user