PHP familiarity shortcuts

This commit is contained in:
udo
2026-04-19 21:15:52 +00:00
parent af642c8167
commit dc05f9faa5
29 changed files with 633 additions and 174 deletions
+4
View File
@@ -188,6 +188,10 @@ RENDER(Request& context)
nibble(s, "-");
?><li><?= (s) ?></li><?
}
else if(layout_class == "sig")
{
?><pre><? print(s); ?></pre><?
}
else if(layout_class == "params")
{
?><div><b><?= trim(nibble(s, ":")) ?></b> : <?= trim(s) ?></div><?
+24
View File
@@ -0,0 +1,24 @@
:sig
StringMap array_merge(StringMap a, StringMap b)
DTree array_merge(DTree a, DTree b)
:params
a : left-hand source map or tree
b : right-hand source map or tree
return value : merged result
:desc
Merges two maps or trees using PHP-like merge behavior.
For `StringMap`, keys from `b` overwrite keys from `a`.
For `DTree`, string keys from `b` overwrite keys from `a`. Numeric keys are appended and reindexed when either side behaves like a list.
This helper is intended as the closest UCE equivalent to PHP `array_merge()` for common request, config, and JSON-shaped data.
:see
>types
:related
**PHP:** `array_merge()`
**JavaScript / Node.js:** Object spread, `Object.assign()`, or array concatenation depending on whether the data is map-like or list-like
+19
View File
@@ -0,0 +1,19 @@
:sig
bool contains(String haystack, String needle)
:params
haystack : string to search in
needle : substring to look for
return value : `true` when `needle` appears in `haystack`, otherwise `false`
:desc
Returns whether `haystack` contains `needle`.
An empty `needle` always returns `true`.
:see
>string
:related
**PHP:** `str_contains()`
**JavaScript / Node.js:** `String.prototype.includes()`
+19
View File
@@ -0,0 +1,19 @@
:sig
void redirect(String url, s32 code = 302)
:params
url : target URL for the redirect
code : optional HTTP redirect status, defaults to `302`
:desc
Sets the `Location` response header and updates the current HTTP status code.
Use this helper instead of manually assigning `context.header["Location"]` and `context.set_status(...)` when you want to redirect the current response.
:see
set_status
0_context
:related
**PHP:** `header("Location: ...")` together with `http_response_code()` or implicit redirect semantics
**JavaScript / Node.js:** `res.redirect(...)`, setting `Location`, or returning a redirect `Response`
+17
View File
@@ -0,0 +1,17 @@
:sig
bool str_ends_with(String haystack, String needle)
:params
haystack : string to inspect
needle : suffix to compare against the end of `haystack`
return value : `true` when `haystack` ends with `needle`, otherwise `false`
:desc
Checks whether `haystack` ends with the given suffix.
:see
>string
:related
**PHP:** `str_ends_with()`
**JavaScript / Node.js:** `String.prototype.endsWith()`
+17
View File
@@ -0,0 +1,17 @@
:sig
bool str_starts_with(String haystack, String needle)
:params
haystack : string to inspect
needle : prefix to compare against the start of `haystack`
return value : `true` when `haystack` begins with `needle`, otherwise `false`
:desc
Checks whether `haystack` starts with the given prefix.
:see
>string
:related
**PHP:** `str_starts_with()`
**JavaScript / Node.js:** `String.prototype.startsWith()`
+22
View File
@@ -0,0 +1,22 @@
:sig
s64 strpos(String haystack, String needle, s64 offset = 0)
:params
haystack : string to search in
needle : substring to search for
offset : optional start offset; negative values count from the end of the string
return value : zero-based position of the first match, or `-1` if `needle` is not found
:desc
Finds the first occurrence of `needle` inside `haystack`.
This is the closest UCE equivalent to PHP `strpos()`, but it returns `-1` instead of `false` when no match is found.
If `needle` is an empty string, `strpos()` returns the normalized start offset.
:see
>string
:related
**PHP:** `strpos()`
**JavaScript / Node.js:** `String.prototype.indexOf()`
+25
View File
@@ -0,0 +1,25 @@
:sig
String substr(String s, s64 start_pos)
String substr(String s, s64 start_pos, s64 length)
:params
s : source string
start_pos : zero-based start position; negative values count from the end of the string
length : optional substring length; negative values trim bytes from the end of the result range
return value : extracted substring, or an empty string if the requested range is outside the source string
:desc
Extracts part of a string using PHP-style start and length semantics.
Use the two-argument form to return everything from `start_pos` to the end of the string.
If `start_pos` is negative, counting starts from the end of the string.
If `length` is negative, the returned range stops that many bytes before the end of the string.
:see
>string
:related
**PHP:** `substr()`
**JavaScript / Node.js:** `String.prototype.slice()` or `substring()`
+3 -9
View File
@@ -1,10 +1,4 @@
// Returns whether haystack contains needle (case-sensitive, both should be pre-lowercased)
bool str_contains(String haystack, String needle)
{
return replace(haystack, needle, "\x01") != haystack;
}
void render_hit(String ft, String name, String badge, String snippet)
{
<><div class="search-hit">
@@ -51,8 +45,8 @@ RENDER(Request& context)
}
String content = file_get_contents("pages/" + ft + ".txt");
bool name_match = str_contains(to_lower(name), q);
bool content_match = str_contains(to_lower(content), q);
bool name_match = contains(to_lower(name), q);
bool content_match = contains(to_lower(content), q);
if(!name_match && !content_match)
continue;
@@ -64,7 +58,7 @@ RENDER(Request& context)
for(auto line : split(content, "\n"))
{
String t = trim(line);
if(t.length() > 4 && t.substr(0, 1) != ":" && str_contains(to_lower(t), q))
if(t.length() > 4 && t.substr(0, 1) != ":" && contains(to_lower(t), q))
{
snippet = t;
if(snippet.length() > 120)
+3 -2
View File
@@ -111,13 +111,14 @@ void render_see_section(String name)
RENDER(Request& context)
{
already_shown_items = new StringMap();
StringMap shown_items;
already_shown_items = &shown_items;
String page = first(context.get["p"], "index");
<><html>
<head>
<!--<link rel="stylesheet" href='style.css?v=<?= time() ?>'></link>-->
<link rel="stylesheet" href='style.css?v=<?= time() ?>'></link>
</head>
<body>
<h1>
+82 -34
View File
@@ -1,24 +1,29 @@
/* UCE Documentation — Theme
Color palette: #139 deep-blue base · white text · yellow accents */
Color palette: deep blue gradient · warm gold accents · frosted glass surfaces */
:root {
--bg: #113399;
--bg-surface: rgba(255, 255, 255, 0.065);
--bg-surface-hover: rgba(255, 255, 255, 0.11);
--bg-inset: rgba(0, 0, 0, 0.22);
--bg-code: rgba(0, 0, 0, 0.28);
--text: #e8ecf4;
--text-dim: rgba(255, 255, 255, 0.55);
--text-muted: rgba(255, 255, 255, 0.35);
--accent: #ffd644;
--accent-dim: rgba(255, 214, 68, 0.1);
--accent-hover: #ffe680;
--border: rgba(255, 255, 255, 0.08);
--border-strong: rgba(255, 255, 255, 0.18);
--font-sans: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
--font-mono: 'SF Mono', 'Cascadia Code', 'Fira Code', Consolas, monospace;
--radius: 8px;
--radius-lg: 14px;
--bg: #0b1d4e;
--bg-grad: linear-gradient(168deg, #0f2462 0%, #0b1d4e 40%, #091840 100%);
--bg-surface: rgba(255, 255, 255, 0.05);
--bg-surface-hover: rgba(255, 255, 255, 0.09);
--bg-inset: rgba(0, 0, 0, 0.3);
--bg-code: rgba(0, 0, 0, 0.32);
--text: #dfe5f2;
--text-dim: rgba(200, 210, 235, 0.6);
--text-muted: rgba(200, 210, 235, 0.35);
--accent: #f0c430;
--accent-dim: rgba(240, 196, 48, 0.08);
--accent-hover: #ffd95c;
--accent-glow: rgba(240, 196, 48, 0.15);
--border: rgba(255, 255, 255, 0.06);
--border-strong: rgba(255, 255, 255, 0.14);
--shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.2);
--shadow-md: 0 4px 16px rgba(0, 0, 0, 0.25);
--shadow-glow: 0 0 20px rgba(240, 196, 48, 0.06);
--font-sans: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
--font-mono: 'JetBrains Mono', 'SF Mono', 'Cascadia Code', 'Fira Code', Consolas, monospace;
--radius: 10px;
--radius-lg: 16px;
--ease: cubic-bezier(0.4, 0, 0.2, 1);
}
@@ -52,8 +57,10 @@ body {
font-family: var(--font-sans);
font-size: 1rem;
line-height: 1.65;
background: var(--bg);
background: var(--bg-grad);
background-attachment: fixed;
color: var(--text);
min-height: 100vh;
}
/* ── Typography ── */
@@ -63,8 +70,9 @@ h1 {
font-size: 1.6rem;
font-weight: 700;
letter-spacing: -0.02em;
padding: 28px 0 20px;
border-bottom: 1px solid var(--border);
padding: 32px 0 20px;
border-bottom: 2px solid transparent;
border-image: linear-gradient(90deg, var(--accent) 0%, rgba(240, 196, 48, 0.15) 100%) 1;
margin-bottom: 28px;
}
@@ -131,15 +139,18 @@ a:hover {
.category {
background: var(--bg-surface);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 14px 18px;
margin-bottom: 10px;
transition: border-color 150ms var(--ease);
transition: border-color 200ms var(--ease), box-shadow 200ms var(--ease);
}
.category:hover {
border-color: var(--border-strong);
box-shadow: var(--shadow-sm);
}
.category h3 {
@@ -190,23 +201,25 @@ a:hover {
display: block;
padding: 7px 12px;
border-radius: 6px;
transition: background 150ms var(--ease);
transition: background 150ms var(--ease), transform 150ms var(--ease);
}
.func-item a:hover {
background: var(--bg-surface-hover);
text-decoration: none;
transform: translateX(2px);
}
.badge {
font-size: 0.68rem;
font-weight: 500;
padding: 1px 7px;
border-radius: 4px;
margin-left: 6px;
font-size: 0.65rem;
font-weight: 600;
padding: 2px 8px;
border-radius: 10px;
margin-left: 8px;
vertical-align: middle;
background: rgba(255, 255, 255, 0.08);
background: rgba(255, 255, 255, 0.06);
color: var(--text-dim);
letter-spacing: 0.02em;
}
.dim {
@@ -221,10 +234,13 @@ a:hover {
.doc-section {
background: var(--bg-surface);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 20px 24px;
margin-bottom: 16px;
box-shadow: var(--shadow-sm);
}
.doc-section h3 {
@@ -240,12 +256,22 @@ a:hover {
line-height: 1.5;
background: var(--bg-code);
border-color: var(--border-strong);
box-shadow: inset 0 1px 4px rgba(0, 0, 0, 0.15);
}
.sig div {
font-family: var(--font-mono);
}
.sig pre {
background: none;
border: none;
padding: 0;
margin: 0;
font-size: inherit;
line-height: inherit;
}
.params div {
padding: 8px 0;
border-bottom: 1px solid var(--border);
@@ -261,6 +287,23 @@ a:hover {
font-weight: 500;
}
.related {
background: var(--accent-dim);
border: 1px solid rgba(240, 196, 48, 0.12);
border-radius: var(--radius);
padding: 14px 20px;
font-size: 0.9rem;
color: var(--text-dim);
}
.related div {
padding: 2px 0;
}
.related strong {
color: var(--text);
}
.see {
background: transparent;
border: none;
@@ -333,9 +376,10 @@ pre {
border: 1px solid var(--border);
border-radius: var(--radius);
font-family: var(--font-mono);
font-size: 0.88rem;
line-height: 1.55;
font-size: 0.86rem;
line-height: 1.6;
white-space: pre-wrap;
box-shadow: inset 0 1px 4px rgba(0, 0, 0, 0.12);
}
code {
@@ -365,9 +409,12 @@ li {
details {
background: var(--bg-surface);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
border: 1px solid var(--border);
border-radius: var(--radius);
margin-bottom: 16px;
box-shadow: var(--shadow-sm);
}
summary {
@@ -421,7 +468,7 @@ details pre {
#doc-search:focus {
outline: none;
border-color: var(--accent);
box-shadow: 0 0 0 3px rgba(255, 214, 68, 0.12);
box-shadow: 0 0 0 3px var(--accent-glow), var(--shadow-glow);
}
.search-count {
@@ -445,13 +492,14 @@ details pre {
display: flex;
align-items: baseline;
gap: 10px;
padding: 5px 12px;
padding: 6px 12px;
border-radius: 6px;
transition: background 120ms var(--ease);
transition: background 120ms var(--ease), transform 120ms var(--ease);
}
.search-hit:hover {
background: var(--bg-surface-hover);
transform: translateX(2px);
}
.search-hit a {
+37 -37
View File
@@ -3,12 +3,6 @@
RENDER(Request& context)
{
DTree t;
DTree assoc_left;
DTree assoc_right;
DTree assoc_merged;
DTree list_left;
DTree list_right;
DTree list_merged;
<>
<link rel="stylesheet" href='style.css'></link>
@@ -17,25 +11,25 @@ RENDER(Request& context)
DTree
</h1>
Value Types:
<h2>Value Types</h2>
<pre><?
t.set("String test");
print(String("String test: ") + t.to_string()+"\n");
print("String: ", t.to_string(), "\n");
t.set(true);
print(String("bool test: ") + t.to_string()+"\n");
print("bool: ", t.to_string(), "\n");
t.set(1234.5678);
print(String("float test: ") + t.to_string()+"\n");
print("float: ", t.to_string(), "\n");
t.set(&t);
print(String("pointer test: ") + t.to_string()+"\n");
print("pointer: ", t.to_string(), "\n");
?></pre>
Tree:
<h2>Nested Tree</h2>
<pre><?
@@ -53,42 +47,48 @@ RENDER(Request& context)
?></pre>
Array Merge
<h2>Array Merge — Associative</h2>
<pre><?
DTree assoc_left;
assoc_left["name"] = "left";
assoc_left["shared"] = "left-value";
DTree assoc_right;
assoc_right["shared"] = "right-value";
assoc_right["extra"] = "new-value";
assoc_merged = array_merge(assoc_left, assoc_right);
print("Associative merge:\n");
print(var_dump(assoc_merged));
list_left.set_array();
DTree a;
a = "A";
list_left.push(a);
DTree b;
b = "B";
list_left.push(b);
list_right.set_array();
DTree c;
c = "C";
list_right.push(c);
DTree d;
d = "D";
list_right.push(d);
list_merged = array_merge(list_left, list_right);
print("\nList merge:\n");
print(var_dump(list_merged));
print("left:\n", var_dump(assoc_left), "\n");
print("right:\n", var_dump(assoc_right), "\n");
print("merged:\n", var_dump(array_merge(assoc_left, assoc_right)));
?></pre>
Params
<pre><?= var_dump(context.params) ?></pre>
<h2>Array Merge — List</h2>
<pre><?
DTree list_left;
list_left.set_array();
DTree a; a = "A"; list_left.push(a);
DTree b; b = "B"; list_left.push(b);
DTree list_right;
list_right.set_array();
DTree c; c = "C"; list_right.push(c);
DTree d; d = "D"; list_right.push(d);
print("left:\n", var_dump(list_left), "\n");
print("right:\n", var_dump(list_right), "\n");
print("merged:\n", var_dump(array_merge(list_left, list_right)));
?></pre>
<details>
<summary>Request Parameters</summary>
<pre><?= var_dump(context.params) ?></pre>
</details>
</>
}
+80 -12
View File
@@ -3,32 +3,100 @@
void test_row(String label, String result)
{
<><tr><td class="test-label"><?= label ?></td><td class="test-result"><code><?= result ?></code></td></tr></>
}
void test_row(String label, String result, String expect)
{
bool pass = (result == expect);
<><tr>
<td class="test-label"><?= label ?></td>
<td class="test-result"><code><?= result ?></code></td>
<td class="test-status <?= pass ? "pass" : "fail" ?>"><?= pass ? "✓" : "✗ expected: " + expect ?></td>
</tr></>
}
void test_num(String label, s64 result, s64 expect)
{
bool pass = (result == expect);
<><tr>
<td class="test-label"><?= label ?></td>
<td class="test-result"><code><? print(result); ?></code></td>
<td class="test-status <?= pass ? "pass" : "fail" ?>"><? if(!pass) { print("✗ expected: "); print(expect); } else { print("✓"); } ?></td>
</tr></>
}
RENDER(Request& context)
{
String sample = " Hello UCE World ";
<>
<link rel="stylesheet" href='style.css'></link>
<style>
table.tests { width: 100%; border-collapse: collapse; margin-bottom: 8px; }
table.tests td { padding: 6px 12px; border-bottom: 1px solid rgba(255,255,255,0.06); font-family: var(--font-mono); font-size: 0.88rem; }
.test-label { color: var(--text-dim); white-space: nowrap; width: 1%; }
.test-result code { background: var(--bg-code); padding: 2px 8px; border-radius: 4px; }
.test-status.pass { color: #6f6; width: 1%; }
.test-status.fail { color: #f66; }
</style>
<h1>
<a href="index.uce">UCE Test</a>:
Strings
</h1>
<pre style="white-space: pre-wrap"><?
<p>sample = <code>"<?= sample ?>"</code></p>
print("sample: [", sample, "]\n");
print("trim(sample): [", trim(sample), "]\n");
print("substr(sample, 2, 5): [", substr(sample, 2, 5), "]\n");
print("substr(sample, -7): [", substr(sample, -7), "]\n");
print("strpos(sample, \"UCE\"): ", strpos(sample, "UCE"), "\n");
print("contains(sample, \"World\"): ", contains(sample, "World") ? "true" : "false", "\n");
print("str_starts_with(trim(sample), \"Hello\"): ", str_starts_with(trim(sample), "Hello") ? "true" : "false", "\n");
print("str_ends_with(trim(sample), \"World\"): ", str_ends_with(trim(sample), "World") ? "true" : "false", "\n");
<h2>trim</h2>
<table class="tests"><?
test_row("trim(sample)", "[" + trim(sample) + "]", "[Hello UCE World]");
?></table>
?></pre>
<h2>substr</h2>
<table class="tests"><?
test_row("substr(sample, 2, 5)", substr(sample, 2, 5), "Hello");
test_row("substr(sample, -7)", substr(sample, -7), "World ");
test_row("substr(sample, 2, -2)", substr(sample, 2, -2), "Hello UCE World");
?></table>
Params
<pre><?= var_dump(context.params) ?></pre>
<h2>strpos</h2>
<table class="tests"><?
test_num("strpos(sample, \"UCE\")", strpos(sample, "UCE"), 8);
test_num("strpos(sample, \"missing\")", strpos(sample, "missing"), -1);
?></table>
<h2>contains</h2>
<table class="tests"><?
test_row("contains(sample, \"World\")", contains(sample, "World") ? "true" : "false", "true");
test_row("contains(sample, \"xyz\")", contains(sample, "xyz") ? "true" : "false", "false");
?></table>
<h2>str_starts_with / str_ends_with</h2>
<table class="tests"><?
test_row("str_starts_with(trim(sample), \"Hello\")", str_starts_with(trim(sample), "Hello") ? "true" : "false", "true");
test_row("str_starts_with(trim(sample), \"World\")", str_starts_with(trim(sample), "World") ? "true" : "false", "false");
test_row("str_ends_with(trim(sample), \"World\")", str_ends_with(trim(sample), "World") ? "true" : "false", "true");
test_row("str_ends_with(trim(sample), \"Hello\")", str_ends_with(trim(sample), "Hello") ? "true" : "false", "false");
?></table>
<h2>replace / split / join</h2>
<table class="tests"><?
test_row("replace(sample, \"UCE\", \"C++\")", replace(sample, "UCE", "C++"), " Hello C++ World ");
test_row("join(split(trim(sample), \" \"), \"-\")", join(split(trim(sample), " "), "-"), "Hello-UCE-World");
?></table>
<h2>to_upper / to_lower</h2>
<table class="tests"><?
test_row("to_upper(trim(sample))", to_upper(trim(sample)), "HELLO UCE WORLD");
test_row("to_lower(trim(sample))", to_lower(trim(sample)), "hello uce world");
?></table>
<details>
<summary>Request Parameters</summary>
<pre><?= var_dump(context.params) ?></pre>
</details>
</>
}
+45 -24
View File
@@ -1,24 +1,28 @@
/* UCE Test Suite — Theme
Color palette: #139 deep-blue base · white text · yellow accents */
Color palette: deep blue gradient · warm gold accents · frosted glass surfaces */
:root {
--bg: #113399;
--bg-surface: rgba(255, 255, 255, 0.065);
--bg-surface-hover: rgba(255, 255, 255, 0.11);
--bg-inset: rgba(0, 0, 0, 0.22);
--bg-code: rgba(0, 0, 0, 0.28);
--text: #e8ecf4;
--text-dim: rgba(255, 255, 255, 0.55);
--text-muted: rgba(255, 255, 255, 0.35);
--accent: #ffd644;
--accent-dim: rgba(255, 214, 68, 0.1);
--accent-hover: #ffe680;
--border: rgba(255, 255, 255, 0.08);
--border-strong: rgba(255, 255, 255, 0.18);
--font-sans: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
--font-mono: 'SF Mono', 'Cascadia Code', 'Fira Code', Consolas, monospace;
--radius: 8px;
--radius-lg: 14px;
--bg: #0b1d4e;
--bg-grad: linear-gradient(168deg, #0f2462 0%, #0b1d4e 40%, #091840 100%);
--bg-surface: rgba(255, 255, 255, 0.05);
--bg-surface-hover: rgba(255, 255, 255, 0.09);
--bg-inset: rgba(0, 0, 0, 0.3);
--bg-code: rgba(0, 0, 0, 0.32);
--text: #dfe5f2;
--text-dim: rgba(200, 210, 235, 0.6);
--text-muted: rgba(200, 210, 235, 0.35);
--accent: #f0c430;
--accent-dim: rgba(240, 196, 48, 0.08);
--accent-hover: #ffd95c;
--accent-glow: rgba(240, 196, 48, 0.15);
--border: rgba(255, 255, 255, 0.06);
--border-strong: rgba(255, 255, 255, 0.14);
--shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.2);
--shadow-md: 0 4px 16px rgba(0, 0, 0, 0.25);
--font-sans: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
--font-mono: 'JetBrains Mono', 'SF Mono', 'Cascadia Code', 'Fira Code', Consolas, monospace;
--radius: 10px;
--radius-lg: 16px;
--ease: cubic-bezier(0.4, 0, 0.2, 1);
}
@@ -52,8 +56,10 @@ body {
font-family: var(--font-sans);
font-size: 1rem;
line-height: 1.65;
background: var(--bg);
background: var(--bg-grad);
background-attachment: fixed;
color: var(--text);
min-height: 100vh;
}
/* ── Typography ── */
@@ -63,8 +69,9 @@ h1 {
font-size: 1.6rem;
font-weight: 700;
letter-spacing: -0.02em;
padding: 28px 0 20px;
border-bottom: 1px solid var(--border);
padding: 32px 0 20px;
border-bottom: 2px solid transparent;
border-image: linear-gradient(90deg, var(--accent) 0%, rgba(240, 196, 48, 0.15) 100%) 1;
margin-bottom: 28px;
}
@@ -117,10 +124,13 @@ body > section,
body > div:not(.test-grid):not(.system-info):not(.grid-heading),
body > p {
background: var(--bg-surface);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 20px 24px;
margin-bottom: 16px;
box-shadow: var(--shadow-sm);
}
/* ── Test Card Grid ── */
@@ -138,18 +148,22 @@ body > p {
gap: 4px;
padding: 16px 20px;
background: var(--bg-surface);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
border: 1px solid var(--border);
border-radius: var(--radius);
text-decoration: none;
color: var(--text);
transition: all 150ms var(--ease);
box-shadow: var(--shadow-sm);
transition: all 200ms var(--ease);
}
.test-card:hover {
background: var(--bg-surface-hover);
border-color: var(--border-strong);
text-decoration: none;
transform: translateY(-1px);
transform: translateY(-2px);
box-shadow: var(--shadow-md);
}
.test-card strong {
@@ -263,8 +277,9 @@ pre {
border-radius: var(--radius);
font-family: var(--font-mono);
font-size: 0.85rem;
line-height: 1.55;
line-height: 1.6;
white-space: pre-wrap;
box-shadow: inset 0 1px 4px rgba(0, 0, 0, 0.12);
}
code {
@@ -302,8 +317,11 @@ li a {
margin-top: 32px;
padding: 16px 20px;
background: var(--bg-code);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
border: 1px solid var(--border);
border-radius: var(--radius);
box-shadow: var(--shadow-sm);
}
.system-info h3 {
@@ -322,9 +340,12 @@ li a {
details {
background: var(--bg-surface);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
border: 1px solid var(--border);
border-radius: var(--radius);
margin-bottom: 16px;
box-shadow: var(--shadow-sm);
}
summary {