@dupecom/botcha-cloudflare 0.10.0 โ 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -8
- package/dist/agents.d.ts +68 -0
- package/dist/agents.d.ts.map +1 -0
- package/dist/agents.js +123 -0
- package/dist/dashboard/api.js +20 -13
- package/dist/dashboard/auth.d.ts.map +1 -1
- package/dist/dashboard/auth.js +6 -6
- package/dist/dashboard/landing.d.ts +20 -0
- package/dist/dashboard/landing.d.ts.map +1 -0
- package/dist/dashboard/landing.js +45 -0
- package/dist/dashboard/layout.d.ts +7 -0
- package/dist/dashboard/layout.d.ts.map +1 -1
- package/dist/dashboard/layout.js +17 -0
- package/dist/dashboard/styles.d.ts +1 -1
- package/dist/dashboard/styles.d.ts.map +1 -1
- package/dist/dashboard/styles.js +99 -1
- package/dist/email.d.ts +0 -3
- package/dist/email.d.ts.map +1 -1
- package/dist/email.js +1 -4
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +461 -212
- package/dist/routes/stream.js +1 -1
- package/dist/static.d.ts +179 -2
- package/dist/static.d.ts.map +1 -1
- package/dist/static.js +319 -5
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,18 +1,19 @@
|
|
|
1
1
|
# @dupecom/botcha-cloudflare
|
|
2
2
|
|
|
3
|
-
>
|
|
3
|
+
> **BOTCHA** - Prove you're a bot. Humans need not apply.
|
|
4
4
|
>
|
|
5
|
-
> **Cloudflare Workers Edition v0.
|
|
5
|
+
> **Cloudflare Workers Edition v0.11.0** - Identity layer for AI agents
|
|
6
6
|
|
|
7
7
|
Reverse CAPTCHA that verifies AI agents and blocks humans. Running at the edge.
|
|
8
8
|
|
|
9
|
-
##
|
|
9
|
+
## What's New in v0.11.0
|
|
10
10
|
|
|
11
|
-
-
|
|
12
|
-
-
|
|
13
|
-
-
|
|
14
|
-
-
|
|
15
|
-
-
|
|
11
|
+
- **Agent Registry** - Persistent agent identities (POST /v1/agents/register)
|
|
12
|
+
- **Email-tied apps** - Email verification, account recovery, secret rotation
|
|
13
|
+
- **Dashboard** - Per-app metrics with agent-first auth (device code flow)
|
|
14
|
+
- **JWT security** - 5-min access tokens, refresh tokens, audience claims, IP binding, revocation
|
|
15
|
+
- **Multi-tenant** - Per-app isolation, scoped tokens, rate limiting
|
|
16
|
+
- **Server-side SDKs** - @botcha/verify (TS) + botcha-verify (Python)
|
|
16
17
|
|
|
17
18
|
## Features
|
|
18
19
|
|
package/dist/agents.d.ts
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BOTCHA Agent Registry
|
|
3
|
+
*
|
|
4
|
+
* Agent registration and management:
|
|
5
|
+
* - Crypto-random agent IDs
|
|
6
|
+
* - KV storage for agent metadata
|
|
7
|
+
* - App-scoped agent lists
|
|
8
|
+
* - Fail-open design for resilience
|
|
9
|
+
*/
|
|
10
|
+
export type KVNamespace = {
|
|
11
|
+
get: (key: string, type?: 'text' | 'json' | 'arrayBuffer' | 'stream') => Promise<any>;
|
|
12
|
+
put: (key: string, value: string, options?: {
|
|
13
|
+
expirationTtl?: number;
|
|
14
|
+
}) => Promise<void>;
|
|
15
|
+
delete: (key: string) => Promise<void>;
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* Agent record stored in KV
|
|
19
|
+
*/
|
|
20
|
+
export interface Agent {
|
|
21
|
+
agent_id: string;
|
|
22
|
+
app_id: string;
|
|
23
|
+
name: string;
|
|
24
|
+
operator?: string;
|
|
25
|
+
version?: string;
|
|
26
|
+
created_at: number;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Generate a crypto-random agent ID
|
|
30
|
+
* Format: 'agent_' + 16 hex chars
|
|
31
|
+
*
|
|
32
|
+
* Example: agent_a1b2c3d4e5f6a7b8
|
|
33
|
+
*/
|
|
34
|
+
export declare function generateAgentId(): string;
|
|
35
|
+
/**
|
|
36
|
+
* Create a new agent and add it to the app's agent list
|
|
37
|
+
*
|
|
38
|
+
* Stores in KV at:
|
|
39
|
+
* - `agent:{agent_id}` โ Agent record
|
|
40
|
+
* - `agents:{app_id}` โ Array of agent_ids for this app
|
|
41
|
+
*
|
|
42
|
+
* @param kv - KV namespace for storage
|
|
43
|
+
* @param app_id - Parent app that owns this agent
|
|
44
|
+
* @param data - Agent metadata (name, operator, version)
|
|
45
|
+
* @returns Agent record with generated agent_id
|
|
46
|
+
*/
|
|
47
|
+
export declare function createAgent(kv: KVNamespace, app_id: string, data: {
|
|
48
|
+
name: string;
|
|
49
|
+
operator?: string;
|
|
50
|
+
version?: string;
|
|
51
|
+
}): Promise<Agent | null>;
|
|
52
|
+
/**
|
|
53
|
+
* Get agent by agent_id
|
|
54
|
+
*
|
|
55
|
+
* @param kv - KV namespace
|
|
56
|
+
* @param agent_id - The agent ID to retrieve
|
|
57
|
+
* @returns Agent record or null if not found
|
|
58
|
+
*/
|
|
59
|
+
export declare function getAgent(kv: KVNamespace, agent_id: string): Promise<Agent | null>;
|
|
60
|
+
/**
|
|
61
|
+
* List all agents for an app
|
|
62
|
+
*
|
|
63
|
+
* @param kv - KV namespace
|
|
64
|
+
* @param app_id - The app ID to list agents for
|
|
65
|
+
* @returns Array of agent records (empty array if none found)
|
|
66
|
+
*/
|
|
67
|
+
export declare function listAgents(kv: KVNamespace, app_id: string): Promise<Agent[]>;
|
|
68
|
+
//# sourceMappingURL=agents.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"agents.d.ts","sourceRoot":"","sources":["../src/agents.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAGH,MAAM,MAAM,WAAW,GAAG;IACxB,GAAG,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,aAAa,GAAG,QAAQ,KAAK,OAAO,CAAC,GAAG,CAAC,CAAC;IACtF,GAAG,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE;QAAE,aAAa,CAAC,EAAE,MAAM,CAAA;KAAE,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACzF,MAAM,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CACxC,CAAC;AAIF;;GAEG;AACH,MAAM,WAAW,KAAK;IACpB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;CACpB;AAID;;;;;GAKG;AACH,wBAAgB,eAAe,IAAI,MAAM,CAOxC;AAID;;;;;;;;;;;GAWG;AACH,wBAAsB,WAAW,CAC/B,EAAE,EAAE,WAAW,EACf,MAAM,EAAE,MAAM,EACd,IAAI,EAAE;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAA;CAAE,GAC1D,OAAO,CAAC,KAAK,GAAG,IAAI,CAAC,CAwCvB;AAED;;;;;;GAMG;AACH,wBAAsB,QAAQ,CAC5B,EAAE,EAAE,WAAW,EACf,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,KAAK,GAAG,IAAI,CAAC,CAcvB;AAED;;;;;;GAMG;AACH,wBAAsB,UAAU,CAC9B,EAAE,EAAE,WAAW,EACf,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,KAAK,EAAE,CAAC,CAsBlB"}
|
package/dist/agents.js
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BOTCHA Agent Registry
|
|
3
|
+
*
|
|
4
|
+
* Agent registration and management:
|
|
5
|
+
* - Crypto-random agent IDs
|
|
6
|
+
* - KV storage for agent metadata
|
|
7
|
+
* - App-scoped agent lists
|
|
8
|
+
* - Fail-open design for resilience
|
|
9
|
+
*/
|
|
10
|
+
// ============ CRYPTO UTILITIES ============
|
|
11
|
+
/**
|
|
12
|
+
* Generate a crypto-random agent ID
|
|
13
|
+
* Format: 'agent_' + 16 hex chars
|
|
14
|
+
*
|
|
15
|
+
* Example: agent_a1b2c3d4e5f6a7b8
|
|
16
|
+
*/
|
|
17
|
+
export function generateAgentId() {
|
|
18
|
+
const bytes = new Uint8Array(8); // 8 bytes = 16 hex chars
|
|
19
|
+
crypto.getRandomValues(bytes);
|
|
20
|
+
const hexString = Array.from(bytes)
|
|
21
|
+
.map(b => b.toString(16).padStart(2, '0'))
|
|
22
|
+
.join('');
|
|
23
|
+
return `agent_${hexString}`;
|
|
24
|
+
}
|
|
25
|
+
// ============ AGENT MANAGEMENT ============
|
|
26
|
+
/**
|
|
27
|
+
* Create a new agent and add it to the app's agent list
|
|
28
|
+
*
|
|
29
|
+
* Stores in KV at:
|
|
30
|
+
* - `agent:{agent_id}` โ Agent record
|
|
31
|
+
* - `agents:{app_id}` โ Array of agent_ids for this app
|
|
32
|
+
*
|
|
33
|
+
* @param kv - KV namespace for storage
|
|
34
|
+
* @param app_id - Parent app that owns this agent
|
|
35
|
+
* @param data - Agent metadata (name, operator, version)
|
|
36
|
+
* @returns Agent record with generated agent_id
|
|
37
|
+
*/
|
|
38
|
+
export async function createAgent(kv, app_id, data) {
|
|
39
|
+
try {
|
|
40
|
+
const agent_id = generateAgentId();
|
|
41
|
+
const agent = {
|
|
42
|
+
agent_id,
|
|
43
|
+
app_id,
|
|
44
|
+
name: data.name,
|
|
45
|
+
operator: data.operator,
|
|
46
|
+
version: data.version,
|
|
47
|
+
created_at: Date.now(),
|
|
48
|
+
};
|
|
49
|
+
// Get existing agent list for this app (if any)
|
|
50
|
+
let agentIds = [];
|
|
51
|
+
try {
|
|
52
|
+
const existingList = await kv.get(`agents:${app_id}`, 'text');
|
|
53
|
+
if (existingList) {
|
|
54
|
+
agentIds = JSON.parse(existingList);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
catch (error) {
|
|
58
|
+
console.warn(`Failed to fetch existing agent list for app ${app_id}, starting fresh:`, error);
|
|
59
|
+
// Fail-open: Continue with empty list
|
|
60
|
+
}
|
|
61
|
+
// Add new agent_id to the list
|
|
62
|
+
agentIds.push(agent_id);
|
|
63
|
+
// Store agent record and updated list in parallel
|
|
64
|
+
await Promise.all([
|
|
65
|
+
kv.put(`agent:${agent_id}`, JSON.stringify(agent)),
|
|
66
|
+
kv.put(`agents:${app_id}`, JSON.stringify(agentIds)),
|
|
67
|
+
]);
|
|
68
|
+
return agent;
|
|
69
|
+
}
|
|
70
|
+
catch (error) {
|
|
71
|
+
console.error(`Failed to create agent for app ${app_id}:`, error);
|
|
72
|
+
// Fail-open: Return null instead of throwing
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Get agent by agent_id
|
|
78
|
+
*
|
|
79
|
+
* @param kv - KV namespace
|
|
80
|
+
* @param agent_id - The agent ID to retrieve
|
|
81
|
+
* @returns Agent record or null if not found
|
|
82
|
+
*/
|
|
83
|
+
export async function getAgent(kv, agent_id) {
|
|
84
|
+
try {
|
|
85
|
+
const data = await kv.get(`agent:${agent_id}`, 'text');
|
|
86
|
+
if (!data) {
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
return JSON.parse(data);
|
|
90
|
+
}
|
|
91
|
+
catch (error) {
|
|
92
|
+
console.error(`Failed to get agent ${agent_id}:`, error);
|
|
93
|
+
// Fail-open: Return null instead of throwing
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* List all agents for an app
|
|
99
|
+
*
|
|
100
|
+
* @param kv - KV namespace
|
|
101
|
+
* @param app_id - The app ID to list agents for
|
|
102
|
+
* @returns Array of agent records (empty array if none found)
|
|
103
|
+
*/
|
|
104
|
+
export async function listAgents(kv, app_id) {
|
|
105
|
+
try {
|
|
106
|
+
// Get the list of agent IDs for this app
|
|
107
|
+
const agentIdsData = await kv.get(`agents:${app_id}`, 'text');
|
|
108
|
+
if (!agentIdsData) {
|
|
109
|
+
return [];
|
|
110
|
+
}
|
|
111
|
+
const agentIds = JSON.parse(agentIdsData);
|
|
112
|
+
// Fetch all agent records in parallel
|
|
113
|
+
const agentPromises = agentIds.map(agent_id => getAgent(kv, agent_id));
|
|
114
|
+
const agents = await Promise.all(agentPromises);
|
|
115
|
+
// Filter out any null results (failed fetches) and return
|
|
116
|
+
return agents.filter((agent) => agent !== null);
|
|
117
|
+
}
|
|
118
|
+
catch (error) {
|
|
119
|
+
console.error(`Failed to list agents for app ${app_id}:`, error);
|
|
120
|
+
// Fail-open: Return empty array instead of throwing
|
|
121
|
+
return [];
|
|
122
|
+
}
|
|
123
|
+
}
|
package/dist/dashboard/api.js
CHANGED
|
@@ -472,8 +472,15 @@ function formatTimeBucket(timestamp, period) {
|
|
|
472
472
|
}
|
|
473
473
|
}
|
|
474
474
|
// ============ MOCK DATA (when CF_API_TOKEN not configured) ============
|
|
475
|
-
function
|
|
476
|
-
return `<div class="
|
|
475
|
+
function renderSampleBanner() {
|
|
476
|
+
return `<div class="sample-banner">Sample data โ analytics will appear here once production traffic flows</div>`;
|
|
477
|
+
}
|
|
478
|
+
function renderSampleLegend(title, period) {
|
|
479
|
+
return `${title} (${period})<span class="sample-tag">SAMPLE</span>`;
|
|
480
|
+
}
|
|
481
|
+
function renderMockOverview(period) {
|
|
482
|
+
return `${renderSampleBanner()}
|
|
483
|
+
<div class="dashboard-grid">
|
|
477
484
|
${renderStatCard('1,247', 'Challenges Generated')}
|
|
478
485
|
${renderStatCard('1,089', 'Verifications')}
|
|
479
486
|
${renderStatCard('94%', 'Success Rate', 'text-success')}
|
|
@@ -482,7 +489,7 @@ function renderMockOverview(_period) {
|
|
|
482
489
|
${renderStatCard('0', 'Errors')}
|
|
483
490
|
</div>`;
|
|
484
491
|
}
|
|
485
|
-
function renderMockVolume(
|
|
492
|
+
function renderMockVolume(period) {
|
|
486
493
|
const items = [
|
|
487
494
|
{ name: '00:00', value: 42, maxValue: 89 },
|
|
488
495
|
{ name: '04:00', value: 15, maxValue: 89 },
|
|
@@ -492,11 +499,11 @@ function renderMockVolume(_period) {
|
|
|
492
499
|
{ name: '20:00', value: 55, maxValue: 89 },
|
|
493
500
|
];
|
|
494
501
|
return `<fieldset>
|
|
495
|
-
<legend
|
|
502
|
+
<legend>${renderSampleLegend('Request Volume', period)}</legend>
|
|
496
503
|
${renderBarChart(items)}
|
|
497
504
|
</fieldset>`;
|
|
498
505
|
}
|
|
499
|
-
function renderMockTypes(
|
|
506
|
+
function renderMockTypes(period) {
|
|
500
507
|
const items = [
|
|
501
508
|
{ name: 'hybrid (412 ok / 18 fail)', value: 430, maxValue: 430 },
|
|
502
509
|
{ name: 'speed (389 ok / 12 fail)', value: 401, maxValue: 430 },
|
|
@@ -504,13 +511,13 @@ function renderMockTypes(_period) {
|
|
|
504
511
|
{ name: 'standard (22 ok / 5 fail)', value: 27, maxValue: 430 },
|
|
505
512
|
];
|
|
506
513
|
return `<fieldset>
|
|
507
|
-
<legend
|
|
514
|
+
<legend>${renderSampleLegend('Challenge Types', period)}</legend>
|
|
508
515
|
${renderBarChart(items)}
|
|
509
516
|
</fieldset>`;
|
|
510
517
|
}
|
|
511
|
-
function renderMockPerformance(
|
|
518
|
+
function renderMockPerformance(period) {
|
|
512
519
|
return `<fieldset>
|
|
513
|
-
<legend
|
|
520
|
+
<legend>${renderSampleLegend('Performance', period)}</legend>
|
|
514
521
|
<table>
|
|
515
522
|
<thead>
|
|
516
523
|
<tr>
|
|
@@ -525,13 +532,13 @@ function renderMockPerformance(_period) {
|
|
|
525
532
|
</table>
|
|
526
533
|
</fieldset>`;
|
|
527
534
|
}
|
|
528
|
-
function renderMockErrors(
|
|
535
|
+
function renderMockErrors(period) {
|
|
529
536
|
return `<fieldset>
|
|
530
|
-
<legend
|
|
531
|
-
<div class="alert alert-success">No errors or rate limits
|
|
537
|
+
<legend>${renderSampleLegend('Errors & Rate Limits', period)}</legend>
|
|
538
|
+
<div class="alert alert-success">No errors or rate limits</div>
|
|
532
539
|
</fieldset>`;
|
|
533
540
|
}
|
|
534
|
-
function renderMockGeo(
|
|
541
|
+
function renderMockGeo(period) {
|
|
535
542
|
const items = [
|
|
536
543
|
{ name: 'US', value: 523, maxValue: 523 },
|
|
537
544
|
{ name: 'DE', value: 189, maxValue: 523 },
|
|
@@ -540,7 +547,7 @@ function renderMockGeo(_period) {
|
|
|
540
547
|
{ name: 'FR', value: 67, maxValue: 523 },
|
|
541
548
|
];
|
|
542
549
|
return `<fieldset>
|
|
543
|
-
<legend
|
|
550
|
+
<legend>${renderSampleLegend('Top Countries', period)}</legend>
|
|
544
551
|
${renderBarChart(items)}
|
|
545
552
|
</fieldset>`;
|
|
546
553
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../../src/dashboard/auth.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,iBAAiB,EAAE,MAAM,MAAM,CAAC;AAMvD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAKjD,KAAK,QAAQ,GAAG;IACd,UAAU,EAAE,WAAW,CAAC;IACxB,WAAW,EAAE,WAAW,CAAC;IACzB,IAAI,EAAE,WAAW,CAAC;IAClB,SAAS,CAAC,EAAE,sBAAsB,CAAC;IACnC,UAAU,EAAE,MAAM,CAAC;IACnB,cAAc,EAAE,MAAM,CAAC;CACxB,CAAC;AAGF,KAAK,SAAS,GAAG;IACf,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB,CAAC;AAsCF;;;;;;;;;GASG;AACH,eAAO,MAAM,oBAAoB,EAAE,iBAAiB,CAAC;IAAE,QAAQ,EAAE,QAAQ,CAAC;IAAC,SAAS,EAAE,SAAS,CAAA;CAAE,CAmChG,CAAC;AAIF;;;;;GAKG;AACH,wBAAsB,4BAA4B,CAAC,CAAC,EAAE,OAAO,CAAC;IAAE,QAAQ,EAAE,QAAQ,CAAA;CAAE,CAAC;;;;;;;;;;oEA2CpF;AAgDD;;;;;GAKG;AACH,wBAAsB,yBAAyB,CAAC,CAAC,EAAE,OAAO,CAAC;IAAE,QAAQ,EAAE,QAAQ,CAAA;CAAE,CAAC;;;;;;;;;;;oEAwBjF;AAID;;;;GAIG;AACH,wBAAsB,yBAAyB,CAAC,CAAC,EAAE,OAAO,CAAC;IAAE,QAAQ,EAAE,QAAQ,CAAA;CAAE,CAAC;;;;;;;;;;oEAEjF;AAED;;;;;GAKG;AACH,wBAAsB,sBAAsB,CAAC,CAAC,EAAE,OAAO,CAAC;IAAE,QAAQ,EAAE,QAAQ,CAAA;CAAE,CAAC;;;;;;;;;;oEA2B9E;AAID;;;;GAIG;AACH,wBAAsB,oBAAoB,CAAC,CAAC,EAAE,OAAO,CAAC;IAAE,QAAQ,EAAE,QAAQ,CAAA;CAAE,CAAC,qBAkE5E;AAED;;;;GAIG;AACH,wBAAsB,sBAAsB,CAAC,CAAC,EAAE,OAAO,CAAC;IAAE,QAAQ,EAAE,QAAQ,CAAA;CAAE,CAAC,gFAqB9E;AAID;;;;;GAKG;AACH,wBAAsB,WAAW,CAAC,CAAC,EAAE,OAAO,CAAC;IAAE,QAAQ,EAAE,QAAQ,CAAA;CAAE,CAAC,gFAyBnE;AAED;;GAEG;AACH,wBAAsB,YAAY,CAAC,CAAC,EAAE,OAAO,CAAC;IAAE,QAAQ,EAAE,QAAQ,CAAA;CAAE,CAAC,gFAGpE;AAID;;;;;;;;;;;;GAYG;AACH,wBAAsB,gBAAgB,CAAC,CAAC,EAAE,OAAO,CAAC;IAAE,QAAQ,EAAE,QAAQ,CAAA;CAAE,CAAC,gFA2BxE;AAID;;;;;;;;GAQG;AACH,wBAAsB,eAAe,CAAC,CAAC,EAAE,OAAO,CAAC;IAAE,QAAQ,EAAE,QAAQ,CAAA;CAAE,CAAC,
|
|
1
|
+
{"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../../src/dashboard/auth.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,iBAAiB,EAAE,MAAM,MAAM,CAAC;AAMvD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAKjD,KAAK,QAAQ,GAAG;IACd,UAAU,EAAE,WAAW,CAAC;IACxB,WAAW,EAAE,WAAW,CAAC;IACzB,IAAI,EAAE,WAAW,CAAC;IAClB,SAAS,CAAC,EAAE,sBAAsB,CAAC;IACnC,UAAU,EAAE,MAAM,CAAC;IACnB,cAAc,EAAE,MAAM,CAAC;CACxB,CAAC;AAGF,KAAK,SAAS,GAAG;IACf,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB,CAAC;AAsCF;;;;;;;;;GASG;AACH,eAAO,MAAM,oBAAoB,EAAE,iBAAiB,CAAC;IAAE,QAAQ,EAAE,QAAQ,CAAC;IAAC,SAAS,EAAE,SAAS,CAAA;CAAE,CAmChG,CAAC;AAIF;;;;;GAKG;AACH,wBAAsB,4BAA4B,CAAC,CAAC,EAAE,OAAO,CAAC;IAAE,QAAQ,EAAE,QAAQ,CAAA;CAAE,CAAC;;;;;;;;;;oEA2CpF;AAgDD;;;;;GAKG;AACH,wBAAsB,yBAAyB,CAAC,CAAC,EAAE,OAAO,CAAC;IAAE,QAAQ,EAAE,QAAQ,CAAA;CAAE,CAAC;;;;;;;;;;;oEAwBjF;AAID;;;;GAIG;AACH,wBAAsB,yBAAyB,CAAC,CAAC,EAAE,OAAO,CAAC;IAAE,QAAQ,EAAE,QAAQ,CAAA;CAAE,CAAC;;;;;;;;;;oEAEjF;AAED;;;;;GAKG;AACH,wBAAsB,sBAAsB,CAAC,CAAC,EAAE,OAAO,CAAC;IAAE,QAAQ,EAAE,QAAQ,CAAA;CAAE,CAAC;;;;;;;;;;oEA2B9E;AAID;;;;GAIG;AACH,wBAAsB,oBAAoB,CAAC,CAAC,EAAE,OAAO,CAAC;IAAE,QAAQ,EAAE,QAAQ,CAAA;CAAE,CAAC,qBAkE5E;AAED;;;;GAIG;AACH,wBAAsB,sBAAsB,CAAC,CAAC,EAAE,OAAO,CAAC;IAAE,QAAQ,EAAE,QAAQ,CAAA;CAAE,CAAC,gFAqB9E;AAID;;;;;GAKG;AACH,wBAAsB,WAAW,CAAC,CAAC,EAAE,OAAO,CAAC;IAAE,QAAQ,EAAE,QAAQ,CAAA;CAAE,CAAC,gFAyBnE;AAED;;GAEG;AACH,wBAAsB,YAAY,CAAC,CAAC,EAAE,OAAO,CAAC;IAAE,QAAQ,EAAE,QAAQ,CAAA;CAAE,CAAC,gFAGpE;AAID;;;;;;;;;;;;GAYG;AACH,wBAAsB,gBAAgB,CAAC,CAAC,EAAE,OAAO,CAAC;IAAE,QAAQ,EAAE,QAAQ,CAAA;CAAE,CAAC,gFA2BxE;AAID;;;;;;;;GAQG;AACH,wBAAsB,eAAe,CAAC,CAAC,EAAE,OAAO,CAAC;IAAE,QAAQ,EAAE,QAAQ,CAAA;CAAE,CAAC,qBA6HvE"}
|
package/dist/dashboard/auth.js
CHANGED
|
@@ -392,10 +392,10 @@ export async function renderLoginPage(c) {
|
|
|
392
392
|
document.querySelector('form').submit();
|
|
393
393
|
}
|
|
394
394
|
`;
|
|
395
|
-
return c.html(_jsxs(LoginLayout, { title: "Dashboard Login - BOTCHA", children: [_jsx("
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
>_
|
|
395
|
+
return c.html(_jsxs(LoginLayout, { title: "Dashboard Login - BOTCHA", children: [_jsx("a", { href: "/", class: "ascii-logo", children: `โโโโโโโ โโโโโโโ โโโโโโโโโ โโโโโโโโโโ โโโ โโโโโโ
|
|
396
|
+
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโ
|
|
397
|
+
โโโโโโโโโโโ โโโ โโโ โโโ โโโโโโโโโโโโโโโโ
|
|
398
|
+
โโโโโโโโโโโ โโโ โโโ โโโ โโโโโโโโโโโโโโโโ
|
|
399
|
+
โโโโโโโโโโโโโโโโโ โโโ โโโโโโโโโโโ โโโโโโ โโโ
|
|
400
|
+
โโโโโโโ โโโโโโโ โโโ โโโโโโโโโโ โโโโโโ โโโ` }), _jsxs("p", { class: "text-muted", style: "text-align: center; font-size: 0.75rem; margin: -1rem 0 2rem;", children: ['>', "_\u00A0prove you're a bot"] }), _jsxs(Card, { title: "Device Code", badge: "agent required", children: [_jsx("p", { class: "text-muted mb-2", style: "font-size: 0.75rem;", children: "Your AI agent can generate a login code for you." }), _jsxs("a", { href: "/dashboard/code", class: "button btn", children: ["Enter Device Code ", '>'] }), _jsxs("div", { class: "hint", children: ["Agent: ", _jsx("code", { children: "POST /v1/auth/device-code" }), " then solve the challenge."] })] }), _jsx(Divider, { text: "or" }), _jsx("form", { method: "post", action: "/dashboard/email-login", children: _jsxs(Card, { title: "Email Login", badge: "returning users", children: [error === 'email_missing' && (_jsx("div", { class: "error-message", children: errorMap[error] })), _jsx("p", { class: "text-muted mb-2", style: "font-size: 0.75rem;", children: "Enter the email you used when creating your app. We'll send a login code to your inbox." }), _jsxs("div", { class: "form-group", children: [_jsx("label", { for: "email", children: "Email" }), _jsx("input", { type: "email", id: "email", name: "email", placeholder: "you@example.com", required: true, autocomplete: "email" })] }), _jsxs("button", { type: "submit", children: ["Email Me a Code ", '>'] })] }) }), _jsx(Divider, { text: "or sign in with credentials" }), _jsx("form", { method: "post", action: "/dashboard/login", children: _jsxs(Card, { title: "App Credentials", children: [error && error !== 'email_missing' && errorMap[error] && (_jsx("div", { class: "error-message", children: errorMap[error] })), _jsxs("div", { id: "create-result", children: [_jsx("div", { class: "warning", children: "Save these credentials now. The secret will not be shown again." }), _jsxs("div", { class: "credentials-box", children: [_jsx("span", { class: "label", children: "app_id: " }), _jsx("span", { class: "value", id: "new-app-id" }), _jsx("br", {}), _jsx("span", { class: "label", children: "secret: " }), _jsx("span", { class: "value", id: "new-app-secret" })] }), _jsxs("button", { type: "button", onclick: "fillAndLogin()", style: "width: 100%; margin-bottom: 1rem;", children: ["Login With New Credentials ", '>'] })] }), _jsxs("div", { class: "form-group", children: [_jsx("label", { for: "app_id", children: "App ID" }), _jsx("input", { type: "text", id: "app_id", name: "app_id", placeholder: "app_...", required: true, autocomplete: "username" })] }), _jsxs("div", { class: "form-group", children: [_jsx("label", { for: "app_secret", children: "App Secret" }), _jsx("input", { type: "password", id: "app_secret", name: "app_secret", placeholder: "sk_...", required: true, autocomplete: "current-password" })] }), _jsxs("div", { style: "display: flex; gap: 0.75rem; align-items: center;", children: [_jsxs("button", { type: "submit", children: ["Login ", '>'] }), _jsxs("button", { type: "button", id: "create-btn", class: "btn-secondary", onclick: "createApp()", children: ["Create App ", '>'] })] })] }) }), _jsx("script", { dangerouslySetInnerHTML: { __html: CREATE_APP_SCRIPT } })] }));
|
|
401
401
|
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BOTCHA Landing Pages (JSX)
|
|
3
|
+
*
|
|
4
|
+
* Two views at GET /:
|
|
5
|
+
* - LandingPage: for unverified human visitors (has token input)
|
|
6
|
+
* - VerifiedLandingPage: for humans whose agent solved the challenge
|
|
7
|
+
*
|
|
8
|
+
* Uses the same terminal aesthetic as the dashboard:
|
|
9
|
+
* JetBrains Mono, dot-shadow Cards, scanline overlay.
|
|
10
|
+
*/
|
|
11
|
+
import type { FC } from 'hono/jsx';
|
|
12
|
+
export declare const LandingPage: FC<{
|
|
13
|
+
version: string;
|
|
14
|
+
error?: string;
|
|
15
|
+
}>;
|
|
16
|
+
export declare const VerifiedLandingPage: FC<{
|
|
17
|
+
version: string;
|
|
18
|
+
solveTime?: number;
|
|
19
|
+
}>;
|
|
20
|
+
//# sourceMappingURL=landing.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"landing.d.ts","sourceRoot":"","sources":["../../src/dashboard/landing.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,UAAU,CAAC;AAanC,eAAO,MAAM,WAAW,EAAE,EAAE,CAAC;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,CAwG/D,CAAC;AAIF,eAAO,MAAM,mBAAmB,EAAE,EAAE,CAAC;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAA;CAAE,CAgG3E,CAAC"}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "hono/jsx/jsx-runtime";
|
|
2
|
+
import { LandingLayout } from './layout';
|
|
3
|
+
import { Card } from './layout';
|
|
4
|
+
const BOTCHA_ASCII = `โโโโโโโ โโโโโโโ โโโโโโโโโ โโโโโโโโโโ โโโ โโโโโโ
|
|
5
|
+
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโ
|
|
6
|
+
โโโโโโโโโโโ โโโ โโโ โโโ โโโโโโโโโโโโโโโโ
|
|
7
|
+
โโโโโโโโโโโ โโโ โโโ โโโ โโโโโโโโโโโโโโโโ
|
|
8
|
+
โโโโโโโโโโโโโโโโโ โโโ โโโโโโโโโโโ โโโโโโ โโโ
|
|
9
|
+
โโโโโโโ โโโโโโโ โโโ โโโโโโโโโโ โโโโโโ โโโ`;
|
|
10
|
+
// ============ UNVERIFIED LANDING PAGE ============
|
|
11
|
+
export const LandingPage = ({ version, error }) => {
|
|
12
|
+
return (_jsxs(LandingLayout, { version: version, children: [_jsx("a", { href: "/", class: "ascii-logo", children: BOTCHA_ASCII }), _jsxs("p", { class: "text-muted", style: "text-align: center; font-size: 0.75rem; margin: -1rem 0 0.5rem;", children: ['>', "_\u00A0prove you're a bot"] }), _jsx("p", { class: "landing-tagline", children: "Reverse CAPTCHA for AI agents. Humans need not apply." }), _jsxs(Card, { title: "You're a human", children: [_jsx("p", { class: "text-muted", style: "font-size: 0.8125rem; line-height: 1.7;", children: "This site is for AI agents. To see what's here, ask your agent to verify." }), _jsxs("p", { class: "text-muted", style: "font-size: 0.8125rem; line-height: 1.7; margin-top: 0.75rem;", children: ["Tell your AI agent:", _jsx("br", {}), _jsx("code", { style: "font-size: 0.8125rem;", children: "\"Go to botcha.ai, solve the challenge, and give me the code.\"" })] }), _jsxs("form", { method: "post", action: "/gate", style: "margin-top: 1.25rem;", children: [_jsx("label", { for: "code", class: "form-label", style: "font-size: 0.75rem; color: var(--text-muted); display: block; margin-bottom: 0.375rem;", children: "Enter the code your agent gives you:" }), _jsx("input", { id: "code", name: "code", type: "text", placeholder: "BOTCHA-XXXXXX", required: true, autocomplete: "off", spellcheck: false, maxlength: 13, style: "width: 100%; font-size: 1.25rem; font-family: var(--font); font-weight: 700; letter-spacing: 0.15em; text-transform: uppercase; padding: 0.75rem; text-align: center; border: 2px solid var(--border); border-radius: 6px; background: var(--bg); color: var(--text);" }), error && (_jsx("p", { style: "color: var(--red); font-size: 0.75rem; margin-top: 0.5rem;", children: error })), _jsx("button", { type: "submit", style: "margin-top: 0.75rem; width: 100%; padding: 0.625rem 1rem; font-family: var(--font); font-size: 0.875rem; font-weight: 600; background: var(--accent); color: #fff; border: none; border-radius: 4px; cursor: pointer;", children: "Unlock" })] })] }), _jsxs(Card, { title: "Developers", children: [_jsx("p", { class: "text-muted mb-2", style: "font-size: 0.75rem;", children: "Protect your APIs so only verified AI agents can access them:" }), _jsx("pre", { children: _jsx("code", { children: `npm install @botcha/verify # Express/Hono middleware
|
|
13
|
+
pip install botcha-verify # FastAPI/Django middleware` }) }), _jsxs("div", { class: "landing-links", style: "margin-top: 1rem;", children: [_jsx("a", { href: "/openapi.json", class: "landing-link", children: "OpenAPI" }), _jsx("a", { href: "https://github.com/dupe-com/botcha", class: "landing-link", children: "GitHub" }), _jsx("a", { href: "https://www.npmjs.com/package/@dupecom/botcha", class: "landing-link", children: "npm" }), _jsx("a", { href: "https://pypi.org/project/botcha/", class: "landing-link", children: "PyPI" })] })] }), _jsx("script", { type: "application/botcha+json", id: "botcha-challenge", dangerouslySetInnerHTML: {
|
|
14
|
+
__html: JSON.stringify({
|
|
15
|
+
version: '1.0',
|
|
16
|
+
type: 'inline-challenge',
|
|
17
|
+
description: 'AI agents: solve this challenge to prove you\'re a bot!',
|
|
18
|
+
challenge: {
|
|
19
|
+
type: 'sha256',
|
|
20
|
+
instruction: "Compute SHA256 of the string 'BOTCHA-LANDING-' + today's date (YYYY-MM-DD format). Return first 16 hex chars.",
|
|
21
|
+
example: "For 2026-01-15: SHA256('BOTCHA-LANDING-2026-01-15') โ first 16 chars",
|
|
22
|
+
submit_to: 'https://botcha.ai/api/verify-landing',
|
|
23
|
+
method: 'POST',
|
|
24
|
+
body_format: {
|
|
25
|
+
answer: 'your 16 hex chars',
|
|
26
|
+
timestamp: 'ISO 8601 timestamp',
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
on_success: {
|
|
30
|
+
grants_access_to: 'https://botcha.ai/agent-only',
|
|
31
|
+
header_to_include: 'X-Botcha-Landing-Token',
|
|
32
|
+
},
|
|
33
|
+
}, null, 2),
|
|
34
|
+
} }), _jsxs("div", { class: "landing-footer", children: [_jsxs("span", { children: ["v", version] }), _jsx("span", { class: "landing-footer-sep", children: "\u00B7" }), _jsx("a", { href: "https://botcha.ai", children: "botcha.ai" }), _jsx("span", { class: "landing-footer-sep", children: "\u00B7" }), _jsx("a", { href: "https://github.com/i8ramin", children: "@i8ramin" })] })] }));
|
|
35
|
+
};
|
|
36
|
+
// ============ VERIFIED LANDING PAGE ============
|
|
37
|
+
export const VerifiedLandingPage = ({ version, solveTime }) => {
|
|
38
|
+
return (_jsxs(LandingLayout, { version: version, children: [_jsx("a", { href: "/", class: "ascii-logo", children: BOTCHA_ASCII }), _jsxs("p", { class: "text-muted", style: "text-align: center; font-size: 0.75rem; margin: -1rem 0 0.5rem;", children: ['>', "_\u00A0verified"] }), _jsxs("p", { class: "landing-tagline", style: "color: var(--green);", children: ["Your agent proved it's a bot", solveTime ? ` in ${solveTime}ms` : '', ". Welcome."] }), _jsxs(Card, { title: "Get started", children: [_jsx("p", { class: "text-muted", style: "font-size: 0.8125rem; line-height: 1.7;", children: "You have an AI agent. Here's what to do with it." }), _jsx("p", { class: "text-muted", style: "font-size: 0.75rem; line-height: 1.6; margin-top: 0.5rem; font-style: italic;", children: "Copy any of these prompts and paste them to your agent:" })] }), _jsxs(Card, { title: "1. Create your app", children: [_jsx("p", { class: "text-muted", style: "font-size: 0.8125rem; line-height: 1.7;", children: "Tell your agent to create a BOTCHA app tied to your email. This gives you an identity on the platform \u2014 your agent gets API keys, you get a dashboard." }), _jsxs("div", { style: "margin-top: 0.75rem; padding: 0.75rem; background: rgba(255,255,255,0.03); border: 1px solid var(--border); border-radius: 6px;", children: [_jsx("p", { style: "font-size: 0.625rem; text-transform: uppercase; letter-spacing: 0.1em; color: var(--text-muted); margin-bottom: 0.375rem;", children: "Say this to your agent:" }), _jsxs("code", { style: "font-size: 0.8125rem; line-height: 1.6; color: var(--accent);", children: ["\"Go to botcha.ai and create an app for me. My email is ", _jsx("span", { contentEditable: "plaintext-only", spellcheck: false, style: "color: var(--green); border-bottom: 1px dashed var(--green); outline: none; min-width: 3ch; padding: 0 2px;", children: "you@example.com" }), ". Save the app_id and app_secret somewhere safe.\""] })] }), _jsx("p", { class: "text-muted", style: "font-size: 0.6875rem; margin-top: 0.5rem;", children: "Your agent will call the API, get your credentials, and a verification code will be emailed to you." })] }), _jsxs(Card, { title: "2. Verify your email", children: [_jsx("p", { class: "text-muted", style: "font-size: 0.8125rem; line-height: 1.7;", children: "Check your inbox for a 6-digit code from BOTCHA. Give it to your agent to confirm your email." }), _jsxs("div", { style: "margin-top: 0.75rem; padding: 0.75rem; background: rgba(255,255,255,0.03); border: 1px solid var(--border); border-radius: 6px;", children: [_jsx("p", { style: "font-size: 0.625rem; text-transform: uppercase; letter-spacing: 0.1em; color: var(--text-muted); margin-bottom: 0.375rem;", children: "Say this to your agent:" }), _jsx("code", { style: "font-size: 0.8125rem; line-height: 1.6; color: var(--accent);", children: "\"The verification code from BOTCHA is [code]. Verify my email.\"" })] }), _jsx("p", { class: "text-muted", style: "font-size: 0.6875rem; margin-top: 0.5rem;", children: "This enables account recovery if you ever lose your credentials." })] }), _jsxs(Card, { title: "3. Open your dashboard", children: [_jsx("p", { class: "text-muted", style: "font-size: 0.8125rem; line-height: 1.7;", children: "Your agent can give you a short code to access your management dashboard \u2014 usage stats, API keys, and settings." }), _jsxs("div", { style: "margin-top: 0.75rem; padding: 0.75rem; background: rgba(255,255,255,0.03); border: 1px solid var(--border); border-radius: 6px;", children: [_jsx("p", { style: "font-size: 0.625rem; text-transform: uppercase; letter-spacing: 0.1em; color: var(--text-muted); margin-bottom: 0.375rem;", children: "Say this to your agent:" }), _jsx("code", { style: "font-size: 0.8125rem; line-height: 1.6; color: var(--accent);", children: "\"Get me a dashboard code for BOTCHA.\"" })] }), _jsxs("p", { class: "text-muted", style: "font-size: 0.6875rem; margin-top: 0.5rem;", children: ["Your agent gives you a ", _jsx("code", { children: "BOTCHA-XXXX" }), " code. Enter it at ", _jsx("a", { href: "/dashboard/code", style: "font-weight: 600;", children: "/dashboard/code" }), "."] })] }), _jsxs(Card, { title: "For developers", children: [_jsx("p", { class: "text-muted", style: "font-size: 0.8125rem; line-height: 1.7; margin-bottom: 0.75rem;", children: "Protect your own APIs so only verified AI agents can access them:" }), _jsx("pre", { children: _jsx("code", { children: `# Client SDK (for your agent)
|
|
39
|
+
npm install @dupecom/botcha # TypeScript
|
|
40
|
+
pip install botcha # Python
|
|
41
|
+
|
|
42
|
+
# Server SDK (protect your APIs)
|
|
43
|
+
npm install @botcha/verify # Express/Hono
|
|
44
|
+
pip install botcha-verify # FastAPI/Django` }) }), _jsxs("div", { class: "landing-links", style: "margin-top: 1rem;", children: [_jsx("a", { href: "/openapi.json", class: "landing-link", children: "OpenAPI" }), _jsx("a", { href: "/ai.txt", class: "landing-link", children: "ai.txt" }), _jsx("a", { href: "https://github.com/dupe-com/botcha", class: "landing-link", children: "GitHub" }), _jsx("a", { href: "https://www.npmjs.com/package/@dupecom/botcha", class: "landing-link", children: "npm" }), _jsx("a", { href: "https://pypi.org/project/botcha/", class: "landing-link", children: "PyPI" })] })] }), _jsxs("div", { class: "landing-footer", children: [_jsxs("span", { children: ["v", version] }), _jsx("span", { class: "landing-footer-sep", children: "\u00B7" }), _jsx("a", { href: "https://botcha.ai", children: "botcha.ai" }), _jsx("span", { class: "landing-footer-sep", children: "\u00B7" }), _jsx("a", { href: "https://github.com/i8ramin", children: "@i8ramin" })] })] }));
|
|
45
|
+
};
|
|
@@ -44,4 +44,11 @@ export declare const DashboardLayout: FC<PropsWithChildren<{
|
|
|
44
44
|
export declare const LoginLayout: FC<PropsWithChildren<{
|
|
45
45
|
title?: string;
|
|
46
46
|
}>>;
|
|
47
|
+
/**
|
|
48
|
+
* Landing page layout โ wider than LoginLayout, includes SEO meta tags.
|
|
49
|
+
* Used for the public landing page at GET /
|
|
50
|
+
*/
|
|
51
|
+
export declare const LandingLayout: FC<PropsWithChildren<{
|
|
52
|
+
version: string;
|
|
53
|
+
}>>;
|
|
47
54
|
//# sourceMappingURL=layout.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"layout.d.ts","sourceRoot":"","sources":["../../src/dashboard/layout.tsx"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,EAAE,EAAE,iBAAiB,EAAE,MAAM,UAAU,CAAC;AAKtD;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,IAAI,EAAE,EAAE,CAAC,iBAAiB,CAAC;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,CAmBzF,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,OAAO,EAAE,EAAE,CAAC;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,CAExC,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,eAAe,EAAE,EAAE,CAAC,iBAAiB,CAAC;IAAE,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,CAmCrF,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,WAAW,EAAE,EAAE,CAAC,iBAAiB,CAAC;IAAE,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,CAsBjE,CAAC"}
|
|
1
|
+
{"version":3,"file":"layout.d.ts","sourceRoot":"","sources":["../../src/dashboard/layout.tsx"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,EAAE,EAAE,iBAAiB,EAAE,MAAM,UAAU,CAAC;AAKtD;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,IAAI,EAAE,EAAE,CAAC,iBAAiB,CAAC;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,CAmBzF,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,OAAO,EAAE,EAAE,CAAC;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,CAExC,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,eAAe,EAAE,EAAE,CAAC,iBAAiB,CAAC;IAAE,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,CAmCrF,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,WAAW,EAAE,EAAE,CAAC,iBAAiB,CAAC;IAAE,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,CAsBjE,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,aAAa,EAAE,EAAE,CAAC,iBAAiB,CAAC;IAAE,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC,CAoDpE,CAAC"}
|
package/dist/dashboard/layout.js
CHANGED
|
@@ -36,3 +36,20 @@ export const DashboardLayout = ({ children, title, appId }) => {
|
|
|
36
36
|
export const LoginLayout = ({ children, title }) => {
|
|
37
37
|
return (_jsxs("html", { lang: "en", children: [_jsxs("head", { children: [_jsx("meta", { charset: "utf-8" }), _jsx("meta", { name: "viewport", content: "width=device-width, initial-scale=1" }), _jsx("title", { children: title || 'BOTCHA Login' }), _jsx("link", { rel: "preconnect", href: "https://fonts.googleapis.com" }), _jsx("link", { href: "https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600;700&display=swap", rel: "stylesheet" }), _jsx("style", { dangerouslySetInnerHTML: { __html: DASHBOARD_CSS } }), _jsx("script", { src: "https://unpkg.com/htmx.org@2.0.4" })] }), _jsx("body", { children: _jsx("div", { class: "login-container", children: _jsx("div", { class: "login-box", children: children }) }) })] }));
|
|
38
38
|
};
|
|
39
|
+
/**
|
|
40
|
+
* Landing page layout โ wider than LoginLayout, includes SEO meta tags.
|
|
41
|
+
* Used for the public landing page at GET /
|
|
42
|
+
*/
|
|
43
|
+
export const LandingLayout = ({ children, version }) => {
|
|
44
|
+
return (_jsxs("html", { lang: "en", children: [_jsxs("head", { children: [_jsx("meta", { charset: "utf-8" }), _jsx("meta", { name: "viewport", content: "width=device-width, initial-scale=1" }), _jsx("title", { children: "BOTCHA \u2014 Reverse CAPTCHA for AI Agents" }), _jsx("meta", { name: "description", content: "BOTCHA is a hosted reverse CAPTCHA that verifies AI agents, not humans. Protect your APIs with computational challenges only bots can solve." }), _jsx("meta", { name: "keywords", content: "AI, bot verification, reverse CAPTCHA, API security, AI agents, agent verification" }), _jsx("link", { rel: "alternate", type: "application/json", href: "/openapi.json", title: "OpenAPI Specification" }), _jsx("link", { rel: "alternate", type: "application/json", href: "/.well-known/ai-plugin.json", title: "AI Plugin Manifest" }), _jsx("link", { rel: "botcha-challenge", href: "#botcha-challenge", type: "application/botcha+json", title: "Embedded Bot Challenge" }), _jsx("meta", { name: "ai-agent-welcome", content: "true" }), _jsx("meta", { name: "botcha-challenge", content: "embedded", "data-selector": "script[type='application/botcha+json']" }), _jsx("meta", { property: "og:title", content: "BOTCHA \u2014 Reverse CAPTCHA for AI Agents" }), _jsx("meta", { property: "og:description", content: "Hosted bot verification service. Prove you're a bot. Humans need not apply." }), _jsx("meta", { property: "og:url", content: "https://botcha.ai" }), _jsx("meta", { property: "og:type", content: "website" }), _jsx("script", { type: "application/ld+json", dangerouslySetInnerHTML: { __html: JSON.stringify({
|
|
45
|
+
'@context': 'https://schema.org',
|
|
46
|
+
'@type': 'SoftwareApplication',
|
|
47
|
+
name: 'BOTCHA',
|
|
48
|
+
applicationCategory: 'DeveloperApplication',
|
|
49
|
+
description: 'Hosted reverse CAPTCHA for AI agents. Computational challenges that only bots can solve.',
|
|
50
|
+
url: 'https://botcha.ai',
|
|
51
|
+
offers: { '@type': 'Offer', price: '0', priceCurrency: 'USD' },
|
|
52
|
+
operatingSystem: 'Any',
|
|
53
|
+
softwareVersion: version,
|
|
54
|
+
}) } }), _jsx("link", { rel: "preconnect", href: "https://fonts.googleapis.com" }), _jsx("link", { href: "https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600;700&display=swap", rel: "stylesheet" }), _jsx("style", { dangerouslySetInnerHTML: { __html: DASHBOARD_CSS } })] }), _jsx("body", { children: _jsx("div", { class: "login-container", children: _jsx("div", { class: "landing-box", children: children }) }) })] }));
|
|
55
|
+
};
|
|
@@ -7,5 +7,5 @@
|
|
|
7
7
|
*
|
|
8
8
|
* JetBrains Mono ยท #ffffff bg ยท #1a1a1a text ยท black accent ยท square corners ยท dot shadows
|
|
9
9
|
*/
|
|
10
|
-
export declare const DASHBOARD_CSS = "\n /* ============ Reset ============ */\n * { margin: 0; padding: 0; box-sizing: border-box; }\n\n :root {\n /* ---- palette (black & white) ---- */\n --bg: #ffffff;\n --bg-card: #ffffff;\n --bg-raised: #eae8e4;\n --text: #1a1a1a;\n --text-muted: #6b6b6b;\n --text-dim: #a0a0a0;\n --accent: #1a1a1a;\n --accent-dim: #333333;\n --red: #cc2222;\n --amber: #b87a00;\n --green: #1a8a2a;\n --border: #ddd9d4;\n --border-bright: #c0bbb5;\n\n /* ---- type ---- */\n --font: 'JetBrains Mono', 'Courier New', monospace;\n\n /* ---- dot shadow (turbopuffer SVG pattern, black fill) ---- */\n --dot-shadow: url(\"data:image/svg+xml,%3Csvg width='7' height='13' viewBox='0 0 7 13' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M5.58984 12.2344V10.7051H6.52734V12.2344H5.58984ZM1.86328 12.2344V10.7051H2.79492V12.2344H1.86328ZM3.72656 10.0957V8.56641H4.6582V10.0957H3.72656ZM0 10.0957V8.56641H0.925781V10.0957H0ZM5.58984 7.95117V6.42188H6.52734V7.95117H5.58984ZM1.86328 7.95117V6.42188H2.79492V7.95117H1.86328ZM3.72656 5.8125V4.2832H4.6582V5.8125H3.72656ZM0 5.8125V4.2832H0.925781V5.8125H0ZM5.58984 3.66797V2.13867H6.52734V3.66797H5.58984ZM1.86328 3.66797V2.13867H2.79492V3.66797H1.86328ZM3.72656 1.5293V0H4.6582V1.5293H3.72656ZM0 1.5293V0H0.925781V1.5293H0Z' fill='%231a1a1a'/%3E%3C/svg%3E\");\n }\n\n /* ============ Base ============ */\n html, body {\n height: 100%;\n font-family: var(--font);\n font-size: 16px;\n line-height: 1.6;\n background: var(--bg);\n color: var(--text);\n -webkit-font-smoothing: antialiased;\n }\n\n body { display: flex; flex-direction: column; }\n\n ::selection { background: var(--accent); color: #fff; }\n\n a { color: var(--accent); }\n a:hover { text-decoration: none; opacity: 0.65; }\n\n /* ============ Scanline overlay (subtle CRT feel) ============ */\n body::before {\n content: '';\n position: fixed;\n inset: 0;\n background: repeating-linear-gradient(\n 0deg,\n transparent,\n transparent 2px,\n rgba(0, 0, 0, 0.012) 2px,\n rgba(0, 0, 0, 0.012) 4px\n );\n pointer-events: none;\n z-index: 9999;\n }\n\n /* ============ Dot shadow utility ============ */\n .dot-shadow { position: relative; }\n .dot-shadow::after {\n content: '';\n position: absolute;\n top: 0.5rem; left: 0.5rem;\n right: -0.5rem; bottom: -0.5rem;\n background-image: var(--dot-shadow);\n background-repeat: repeat;\n z-index: -1;\n pointer-events: none;\n opacity: 0.6;\n }\n\n /* ============ Navigation ============ */\n .dashboard-nav {\n background: var(--bg);\n border-bottom: 1px solid var(--border);\n position: sticky; top: 0; z-index: 100;\n }\n\n .nav-container {\n max-width: 1200px;\n margin: 0 auto;\n padding: 0.75rem 1.5rem;\n display: flex;\n align-items: center;\n gap: 1.5rem;\n }\n\n .nav-logo {\n font-weight: 700;\n font-size: 0.875rem;\n color: var(--text);\n text-decoration: none;\n letter-spacing: 0.15em;\n text-transform: uppercase;\n }\n .nav-logo:hover { opacity: 1; }\n\n .nav-app-id {\n color: var(--text-muted);\n font-size: 0.75rem;\n margin-left: auto;\n }\n\n .nav-link {\n color: var(--text);\n text-decoration: none;\n font-size: 0.75rem;\n border: 1px solid var(--border-bright);\n padding: 0.25rem 0.75rem;\n text-transform: uppercase;\n letter-spacing: 0.05em;\n transition: background 0.1s, color 0.1s;\n }\n .nav-link:hover {\n background: var(--accent);\n color: var(--bg);\n border-color: var(--accent);\n opacity: 1;\n }\n\n /* ============ Main content ============ */\n .dashboard-main {\n flex: 1;\n max-width: 1200px;\n width: 100%;\n margin: 0 auto;\n padding: 2rem 1.5rem;\n }\n\n /* ============ Card \u2014 primary container (Turbopuffer-style) ============ */\n .card {\n display: flex;\n flex-direction: column;\n margin-bottom: 1.5rem;\n }\n\n .card-header {\n margin-bottom: -1px; /* overlap the border */\n padding: 0;\n }\n\n .card-header h3 {\n position: relative;\n display: inline-flex;\n align-items: center;\n z-index: 10;\n top: 0.5rem;\n left: 0.5rem;\n font-size: 0.75rem;\n font-weight: 700;\n text-transform: uppercase;\n letter-spacing: 0.1em;\n line-height: 1;\n color: var(--text);\n background: var(--bg);\n margin: 0;\n padding: 0 0.5rem;\n }\n\n .card-header h3 .card-title {\n }\n\n .card-header h3 .badge-inline {\n }\n\n .card-body {\n position: relative;\n border: 2px solid var(--border-bright);\n }\n\n .card-body::before {\n content: '';\n position: absolute;\n top: 0.5rem;\n left: 0.5rem;\n right: -0.5rem;\n bottom: -0.5rem;\n background-image: var(--dot-shadow);\n background-repeat: repeat;\n pointer-events: none;\n opacity: 0.6;\n }\n\n .card-inner {\n position: relative;\n z-index: 1;\n background: var(--bg-card);\n padding: 1.5rem;\n }\n\n /* Legacy fieldset support (deprecated \u2014 use .card instead) */\n fieldset {\n border: 2px solid var(--border-bright);\n border-radius: 0;\n padding: 1.5rem;\n margin-bottom: 1.5rem;\n background: var(--bg-card);\n position: relative;\n z-index: 0;\n }\n\n legend {\n padding: 0 0.5rem;\n font-size: 0.75rem;\n color: var(--text);\n font-weight: 700;\n text-transform: uppercase;\n letter-spacing: 0.1em;\n }\n\n /* ============ Dashboard grid ============ */\n .dashboard-grid {\n display: grid;\n grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));\n gap: 1.5rem;\n margin-bottom: 1.5rem;\n }\n\n /* ============ Stat cards ============ */\n .stat-card {\n display: flex; flex-direction: column; gap: 0.25rem;\n padding: 1rem;\n border: 1px solid var(--border);\n background: var(--bg-card);\n }\n\n .stat-card .stat-value {\n font-size: 2rem; font-weight: 700; line-height: 1;\n color: var(--text);\n font-variant-numeric: tabular-nums;\n }\n\n .stat-card .stat-label {\n font-size: 0.6875rem;\n color: var(--text-muted);\n text-transform: uppercase;\n letter-spacing: 0.1em;\n }\n\n .stat-card .stat-change { font-size: 0.75rem; font-weight: 500; }\n .stat-card .stat-change.positive { color: var(--green); }\n .stat-card .stat-change.negative { color: var(--red); }\n\n /* ============ Bar chart ============ */\n .bar-chart { width: 100%; }\n\n .bar-chart .bar-item { margin-bottom: 0.75rem; }\n\n .bar-chart .bar-label {\n display: flex; justify-content: space-between; align-items: center;\n margin-bottom: 0.125rem; font-size: 0.75rem;\n }\n .bar-chart .bar-name { color: var(--text); }\n .bar-chart .bar-value {\n color: var(--text-muted);\n font-weight: 700; font-variant-numeric: tabular-nums;\n }\n\n .bar-chart .bar {\n height: 18px;\n background: var(--accent);\n border-radius: 0;\n transition: width 0.3s ease;\n position: relative; overflow: hidden;\n opacity: 0.8;\n }\n .bar-chart .bar:hover { opacity: 1; }\n\n .bar-chart .bar-fill { height: 100%; background: var(--accent); }\n\n /* ============ Form controls ============ */\n input, select, textarea, button { font-family: var(--font); font-size: 0.875rem; }\n\n input[type=\"text\"],\n input[type=\"email\"],\n input[type=\"password\"],\n input[type=\"number\"],\n select,\n textarea {\n width: 100%;\n padding: 0.625rem 0.75rem;\n background: var(--bg);\n border: 1px solid var(--border-bright);\n border-radius: 0;\n color: var(--text);\n }\n\n input:focus, select:focus, textarea:focus {\n outline: none;\n border-color: var(--accent);\n box-shadow: 0 0 0 1px var(--accent);\n }\n\n input::placeholder, textarea::placeholder { color: var(--text-dim); }\n\n label {\n display: block;\n margin-bottom: 0.375rem;\n font-size: 0.6875rem;\n color: var(--text-muted);\n font-weight: 700;\n text-transform: uppercase;\n letter-spacing: 0.08em;\n }\n\n .form-group { margin-bottom: 1.25rem; }\n\n /* ============ Buttons ============ */\n button, .button {\n display: inline-block;\n padding: 0.625rem 1.25rem;\n background: var(--accent);\n color: #fff;\n border: 1px solid var(--accent);\n border-radius: 0;\n font-weight: 700;\n cursor: pointer;\n text-decoration: none;\n text-align: center;\n text-transform: uppercase;\n letter-spacing: 0.08em;\n font-size: 0.75rem;\n box-shadow:\n inset 1px 1px 0 rgba(255,255,255,0.15),\n inset -1px -1px 0 rgba(0,0,0,0.15),\n 2px 2px 0 rgba(0,0,0,0.1);\n transition: box-shadow 0.1s, transform 0.1s;\n }\n button:hover, .button:hover {\n box-shadow:\n inset 1px 1px 0 rgba(255,255,255,0.1),\n inset -1px -1px 0 rgba(0,0,0,0.15),\n 3px 3px 0 rgba(0,0,0,0.12);\n opacity: 1;\n }\n button:active, .button:active {\n transform: translate(1px, 1px);\n box-shadow: inset 1px 1px 3px rgba(0,0,0,0.25);\n }\n button:disabled, .button:disabled { opacity: 0.25; cursor: not-allowed; }\n\n button.secondary, .button.secondary {\n background: transparent;\n color: var(--text);\n border-color: var(--border-bright);\n box-shadow: 2px 2px 0 rgba(0,0,0,0.05);\n }\n button.secondary:hover, .button.secondary:hover {\n border-color: var(--accent);\n color: var(--accent);\n box-shadow: 2px 2px 0 rgba(0,0,0,0.1);\n }\n\n button.danger, .button.danger {\n background: var(--red);\n border-color: var(--red);\n color: #fff;\n box-shadow: 2px 2px 0 rgba(204,34,34,0.15);\n }\n button.danger:hover, .button.danger:hover {\n background: transparent;\n color: var(--red);\n }\n\n /* ============ Tables ============ */\n table { width: 100%; border-collapse: collapse; }\n\n thead { border-bottom: 1px solid var(--border-bright); }\n th {\n padding: 0.5rem 0.75rem; text-align: left;\n font-size: 0.6875rem; color: var(--text);\n font-weight: 700; text-transform: uppercase;\n letter-spacing: 0.1em;\n background: var(--bg-raised);\n }\n\n td {\n padding: 0.5rem 0.75rem;\n border-bottom: 1px solid var(--border);\n font-size: 0.75rem;\n font-variant-numeric: tabular-nums;\n }\n\n tbody tr:hover { background: var(--bg-raised); }\n\n /* ============ Code ============ */\n code, pre {\n font-family: var(--font);\n background: var(--bg-raised);\n padding: 0.125rem 0.375rem;\n border-radius: 0;\n font-size: 0.75rem;\n border: 1px solid var(--border);\n color: var(--text);\n }\n pre { padding: 1rem; overflow-x: auto; border: 1px solid var(--border-bright); }\n pre code { background: none; padding: 0; border: none; }\n\n /* ============ Login layout ============ */\n .login-container {\n min-height: 100vh;\n display: flex; align-items: center; justify-content: center;\n padding: 2rem;\n background: var(--bg);\n }\n .login-box { width: 100%; max-width: 420px; }\n\n .login-header { text-align: center; margin-bottom: 2rem; }\n .login-header h1 {\n font-size: 1.5rem; font-weight: 700; color: var(--text);\n letter-spacing: 0.15em; text-transform: uppercase;\n margin-bottom: 0.25rem;\n }\n .login-header p { color: var(--text-muted); font-size: 0.75rem; }\n\n /* ============ htmx loading ============ */\n .htmx-indicator { opacity: 0; transition: opacity 0.15s; }\n .htmx-request .htmx-indicator { opacity: 1; }\n .htmx-request.htmx-swapping { opacity: 0.3; pointer-events: none; }\n\n /* ============ Skeleton \u2014 blinking cursor ============ */\n .skeleton {\n background: var(--bg-raised);\n border: 1px solid var(--border);\n position: relative;\n overflow: hidden;\n }\n .skeleton::after {\n content: '';\n position: absolute; left: 0; top: 0;\n width: 2px; height: 100%;\n background: var(--text);\n animation: cursor-blink 0.8s step-end infinite;\n }\n @keyframes cursor-blink {\n 0%, 100% { opacity: 1; }\n 50% { opacity: 0; }\n }\n\n .skeleton-text { height: 1rem; margin-bottom: 0.5rem; }\n .skeleton-heading { height: 1.5rem; width: 60%; margin-bottom: 1rem; }\n\n /* ============ Utilities ============ */\n .text-center { text-align: center; }\n .text-right { text-align: right; }\n .text-muted { color: var(--text-muted); }\n .text-dim { color: var(--text-dim); }\n .text-success { color: var(--green); }\n .text-danger { color: var(--red); }\n .text-warning { color: var(--amber); }\n\n .mb-0 { margin-bottom: 0; }\n .mb-1 { margin-bottom: 0.5rem; }\n .mb-2 { margin-bottom: 1rem; }\n .mb-3 { margin-bottom: 1.5rem; }\n .mb-4 { margin-bottom: 2rem; }\n .mt-0 { margin-top: 0; }\n .mt-1 { margin-top: 0.5rem; }\n .mt-2 { margin-top: 1rem; }\n .mt-3 { margin-top: 1.5rem; }\n .mt-4 { margin-top: 2rem; }\n\n /* ============ Period selector ============ */\n .period-selector button {\n font-size: 0.625rem;\n padding: 0.2rem 0.5rem;\n }\n\n /* ============ Responsive ============ */\n @media (max-width: 768px) {\n html, body { font-size: 14px; }\n .dashboard-main { padding: 1rem; }\n .nav-container { padding: 0.5rem 1rem; }\n .dashboard-grid { grid-template-columns: 1fr; gap: 1rem; }\n .card-inner { padding: 1rem; }\n .card { margin-bottom: 1rem; }\n fieldset { padding: 1rem; margin-bottom: 1rem; }\n .stat-card .stat-value { font-size: 1.75rem; }\n table { font-size: 0.625rem; }\n th, td { padding: 0.375rem 0.5rem; }\n }\n\n @media (max-width: 480px) {\n .nav-container { flex-wrap: wrap; }\n .nav-app-id { margin-left: 0; width: 100%; order: 3; }\n }\n\n /* ============ Alerts ============ */\n .alert {\n padding: 0.75rem 1rem;\n border-radius: 0;\n margin-bottom: 1.5rem;\n border: 1px solid var(--border-bright);\n font-size: 0.75rem;\n background: var(--bg-card);\n }\n .alert::before { font-weight: 700; margin-right: 0.5rem; }\n\n .alert-info { border-color: var(--border-bright); color: var(--text); }\n .alert-info::before { content: '>'; color: var(--text); }\n\n .alert-success { border-color: var(--green); color: var(--green); }\n .alert-success::before { content: '[ok]'; }\n\n .alert-warning { border-color: var(--amber); color: var(--amber); }\n .alert-warning::before { content: '[!!]'; }\n\n .alert-danger { border-color: var(--red); color: var(--red); }\n .alert-danger::before { content: '[ERR]'; }\n\n /* ============ Badges ============ */\n .badge {\n display: inline-block;\n padding: 0.125rem 0.375rem;\n font-size: 0.625rem; font-weight: 700;\n border-radius: 0;\n text-transform: uppercase;\n letter-spacing: 0.05em;\n border: 1px solid;\n }\n .badge-success { color: var(--green); border-color: var(--green); background: transparent; }\n .badge-danger { color: var(--red); border-color: var(--red); background: transparent; }\n .badge-warning { color: var(--amber); border-color: var(--amber); background: transparent; }\n .badge-info { color: #fff; border-color: var(--accent); background: var(--accent); }\n\n /* ============ Empty state ============ */\n .empty-state { text-align: center; padding: 3rem 1rem; color: var(--text-muted); }\n .empty-state-icon { font-size: 1.5rem; margin-bottom: 0.75rem; color: var(--text); font-weight: 700; }\n .empty-state-text { font-size: 0.8125rem; margin-bottom: 0.25rem; }\n .empty-state-subtext { font-size: 0.6875rem; color: var(--text-dim); }\n\n /* ============ Auth pages ============ */\n .ascii-logo {\n text-align: center; margin-bottom: 2rem;\n color: var(--text); font-size: 0.55rem; line-height: 1.2;\n white-space: pre; font-weight: 400;\n }\n\n .badge-inline {\n display: inline-block; font-size: 0.5625rem; font-weight: 700;\n color: var(--text-muted); border: 1px solid var(--border-bright);\n border-radius: 0; padding: 0.1rem 0.4rem;\n margin-left: 0.5rem; vertical-align: middle;\n text-transform: uppercase; letter-spacing: 0.05em;\n }\n\n .divider {\n display: flex; align-items: center; gap: 0.75rem;\n color: var(--text-dim); font-size: 0.6875rem;\n margin: 1.5rem 0;\n text-transform: uppercase; letter-spacing: 0.1em;\n white-space: nowrap;\n }\n .divider::before, .divider::after {\n content: ''; flex: 1;\n height: 1px; background: var(--border-bright);\n }\n\n .credentials-box {\n background: var(--bg); border: 1px solid var(--accent-dim);\n padding: 1rem; margin-bottom: 1rem;\n font-size: 0.75rem; line-height: 1.8; word-break: break-all;\n }\n .credentials-box .label { color: var(--text-muted); }\n .credentials-box .value { color: var(--text); font-weight: 700; }\n\n .warning {\n background: rgba(184,122,0,0.06); border: 1px solid var(--amber);\n padding: 0.75rem; margin-bottom: 1rem;\n font-size: 0.7rem; color: var(--amber);\n }\n .warning::before { content: '[!!] '; font-weight: 700; }\n\n .error-message {\n color: var(--red); margin: 0 0 1rem 0; font-size: 0.75rem;\n padding: 0.5rem 0.75rem;\n border: 1px solid rgba(204,34,34,0.3);\n background: var(--bg);\n }\n .error-message::before { content: '[ERR] '; font-weight: 700; }\n\n .hint {\n font-size: 0.6875rem; color: var(--text-muted); line-height: 1.6;\n margin-top: 0.75rem;\n }\n .hint code {\n color: var(--text); background: var(--bg-raised);\n padding: 0.125rem 0.375rem; border: 1px solid var(--border);\n }\n\n .btn {\n display: block; width: 100%; text-align: center; text-decoration: none;\n }\n .btn-secondary {\n background: transparent; color: var(--text);\n border-color: var(--border-bright);\n box-shadow: 2px 2px 0 rgba(0,0,0,0.05);\n }\n .btn-secondary:hover {\n border-color: var(--accent); color: var(--accent);\n box-shadow: 2px 2px 0 rgba(0,0,0,0.1);\n }\n\n #create-result { display: none; }\n #create-result.show { display: block; }\n #create-btn.loading { opacity: 0.25; pointer-events: none; }\n\n /* ============ Scrollbar ============ */\n ::-webkit-scrollbar { width: 6px; height: 6px; }\n ::-webkit-scrollbar-track { background: var(--bg-raised); }\n ::-webkit-scrollbar-thumb { background: var(--border-bright); }\n ::-webkit-scrollbar-thumb:hover { background: var(--text-muted); }\n\n /* ============ Responsive (small screens) ============ */\n @media (max-width: 480px) {\n .ascii-logo { font-size: 0.4rem; }\n .login-container { padding: 1rem; }\n .card-inner { padding: 1rem; }\n }\n";
|
|
10
|
+
export declare const DASHBOARD_CSS = "\n /* ============ Reset ============ */\n * { margin: 0; padding: 0; box-sizing: border-box; }\n\n :root {\n /* ---- palette (black & white) ---- */\n --bg: #ffffff;\n --bg-card: #ffffff;\n --bg-raised: #eae8e4;\n --text: #1a1a1a;\n --text-muted: #6b6b6b;\n --text-dim: #a0a0a0;\n --accent: #1a1a1a;\n --accent-dim: #333333;\n --red: #cc2222;\n --amber: #b87a00;\n --green: #1a8a2a;\n --border: #ddd9d4;\n --border-bright: #c0bbb5;\n\n /* ---- type ---- */\n --font: 'JetBrains Mono', 'Courier New', monospace;\n\n /* ---- dot shadow (turbopuffer SVG pattern, black fill) ---- */\n --dot-shadow: url(\"data:image/svg+xml,%3Csvg width='7' height='13' viewBox='0 0 7 13' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M5.58984 12.2344V10.7051H6.52734V12.2344H5.58984ZM1.86328 12.2344V10.7051H2.79492V12.2344H1.86328ZM3.72656 10.0957V8.56641H4.6582V10.0957H3.72656ZM0 10.0957V8.56641H0.925781V10.0957H0ZM5.58984 7.95117V6.42188H6.52734V7.95117H5.58984ZM1.86328 7.95117V6.42188H2.79492V7.95117H1.86328ZM3.72656 5.8125V4.2832H4.6582V5.8125H3.72656ZM0 5.8125V4.2832H0.925781V5.8125H0ZM5.58984 3.66797V2.13867H6.52734V3.66797H5.58984ZM1.86328 3.66797V2.13867H2.79492V3.66797H1.86328ZM3.72656 1.5293V0H4.6582V1.5293H3.72656ZM0 1.5293V0H0.925781V1.5293H0Z' fill='%231a1a1a'/%3E%3C/svg%3E\");\n }\n\n /* ============ Base ============ */\n html, body {\n height: 100%;\n font-family: var(--font);\n font-size: 16px;\n line-height: 1.6;\n background: var(--bg);\n color: var(--text);\n -webkit-font-smoothing: antialiased;\n }\n\n body { display: flex; flex-direction: column; }\n\n ::selection { background: var(--accent); color: #fff; }\n\n a { color: var(--accent); }\n a:hover { text-decoration: none; opacity: 0.65; }\n\n /* ============ Scanline overlay (subtle CRT feel) ============ */\n body::before {\n content: '';\n position: fixed;\n inset: 0;\n background: repeating-linear-gradient(\n 0deg,\n transparent,\n transparent 2px,\n rgba(0, 0, 0, 0.012) 2px,\n rgba(0, 0, 0, 0.012) 4px\n );\n pointer-events: none;\n z-index: 9999;\n }\n\n /* ============ Dot shadow utility ============ */\n .dot-shadow { position: relative; }\n .dot-shadow::after {\n content: '';\n position: absolute;\n top: 0.5rem; left: 0.5rem;\n right: -0.5rem; bottom: -0.5rem;\n background-image: var(--dot-shadow);\n background-repeat: repeat;\n z-index: -1;\n pointer-events: none;\n opacity: 0.6;\n }\n\n /* ============ Navigation ============ */\n .dashboard-nav {\n background: var(--bg);\n border-bottom: 1px solid var(--border);\n position: sticky; top: 0; z-index: 100;\n }\n\n .nav-container {\n max-width: 1200px;\n margin: 0 auto;\n padding: 0.75rem 1.5rem;\n display: flex;\n align-items: center;\n gap: 1.5rem;\n }\n\n .nav-logo {\n font-weight: 700;\n font-size: 0.875rem;\n color: var(--text);\n text-decoration: none;\n letter-spacing: 0.15em;\n text-transform: uppercase;\n }\n .nav-logo:hover { opacity: 1; }\n\n .nav-app-id {\n color: var(--text-muted);\n font-size: 0.75rem;\n margin-left: auto;\n }\n\n .nav-link {\n color: var(--text);\n text-decoration: none;\n font-size: 0.75rem;\n border: 1px solid var(--border-bright);\n padding: 0.25rem 0.75rem;\n text-transform: uppercase;\n letter-spacing: 0.05em;\n transition: background 0.1s, color 0.1s;\n }\n .nav-link:hover {\n background: var(--accent);\n color: var(--bg);\n border-color: var(--accent);\n opacity: 1;\n }\n\n /* ============ Main content ============ */\n .dashboard-main {\n flex: 1;\n max-width: 1200px;\n width: 100%;\n margin: 0 auto;\n padding: 2rem 1.5rem;\n }\n\n /* ============ Card \u2014 primary container (Turbopuffer-style) ============ */\n .card {\n display: flex;\n flex-direction: column;\n margin-bottom: 1.5rem;\n }\n\n .card-header {\n margin-bottom: -1px; /* overlap the border */\n padding: 0;\n }\n\n .card-header h3 {\n position: relative;\n display: inline-flex;\n align-items: center;\n z-index: 10;\n top: 0.5rem;\n left: 0.5rem;\n font-size: 0.75rem;\n font-weight: 700;\n text-transform: uppercase;\n letter-spacing: 0.1em;\n line-height: 1;\n color: var(--text);\n background: var(--bg);\n margin: 0;\n padding: 0 0.5rem;\n }\n\n .card-header h3 .card-title {\n }\n\n .card-header h3 .badge-inline {\n }\n\n .card-body {\n position: relative;\n border: 2px solid var(--border-bright);\n }\n\n .card-body::before {\n content: '';\n position: absolute;\n top: 0.5rem;\n left: 0.5rem;\n right: -0.5rem;\n bottom: -0.5rem;\n background-image: var(--dot-shadow);\n background-repeat: repeat;\n pointer-events: none;\n opacity: 0.6;\n }\n\n .card-inner {\n position: relative;\n z-index: 1;\n background: var(--bg-card);\n padding: 1.5rem;\n }\n\n /* Legacy fieldset support (deprecated \u2014 use .card instead) */\n fieldset {\n border: 2px solid var(--border-bright);\n border-radius: 0;\n padding: 1.5rem;\n margin-bottom: 1.5rem;\n background: var(--bg-card);\n position: relative;\n z-index: 0;\n }\n\n legend {\n padding: 0 0.5rem;\n font-size: 0.75rem;\n color: var(--text);\n font-weight: 700;\n text-transform: uppercase;\n letter-spacing: 0.1em;\n }\n\n /* ============ Dashboard grid ============ */\n .dashboard-grid {\n display: grid;\n grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));\n gap: 1.5rem;\n margin-bottom: 1.5rem;\n }\n\n /* ============ Stat cards ============ */\n .stat-card {\n display: flex; flex-direction: column; gap: 0.25rem;\n padding: 1rem;\n border: 1px solid var(--border);\n background: var(--bg-card);\n }\n\n .stat-card .stat-value {\n font-size: 2rem; font-weight: 700; line-height: 1;\n color: var(--text);\n font-variant-numeric: tabular-nums;\n }\n\n .stat-card .stat-label {\n font-size: 0.6875rem;\n color: var(--text-muted);\n text-transform: uppercase;\n letter-spacing: 0.1em;\n }\n\n .stat-card .stat-change { font-size: 0.75rem; font-weight: 500; }\n .stat-card .stat-change.positive { color: var(--green); }\n .stat-card .stat-change.negative { color: var(--red); }\n\n /* ============ Bar chart ============ */\n .bar-chart { width: 100%; }\n\n .bar-chart .bar-item { margin-bottom: 0.75rem; }\n\n .bar-chart .bar-label {\n display: flex; justify-content: space-between; align-items: center;\n margin-bottom: 0.125rem; font-size: 0.75rem;\n }\n .bar-chart .bar-name { color: var(--text); }\n .bar-chart .bar-value {\n color: var(--text-muted);\n font-weight: 700; font-variant-numeric: tabular-nums;\n }\n\n .bar-chart .bar {\n height: 18px;\n background: var(--accent);\n border-radius: 0;\n transition: width 0.3s ease;\n position: relative; overflow: hidden;\n opacity: 0.8;\n }\n .bar-chart .bar:hover { opacity: 1; }\n\n .bar-chart .bar-fill { height: 100%; background: var(--accent); }\n\n /* ============ Form controls ============ */\n input, select, textarea, button { font-family: var(--font); font-size: 0.875rem; }\n\n input[type=\"text\"],\n input[type=\"email\"],\n input[type=\"password\"],\n input[type=\"number\"],\n select,\n textarea {\n width: 100%;\n padding: 0.625rem 0.75rem;\n background: var(--bg);\n border: 1px solid var(--border-bright);\n border-radius: 0;\n color: var(--text);\n }\n\n input:focus, select:focus, textarea:focus {\n outline: none;\n border-color: var(--accent);\n box-shadow: 0 0 0 1px var(--accent);\n }\n\n input::placeholder, textarea::placeholder { color: var(--text-dim); }\n\n label {\n display: block;\n margin-bottom: 0.375rem;\n font-size: 0.6875rem;\n color: var(--text-muted);\n font-weight: 700;\n text-transform: uppercase;\n letter-spacing: 0.08em;\n }\n\n .form-group { margin-bottom: 1.25rem; }\n\n /* ============ Buttons ============ */\n button, .button {\n display: inline-block;\n padding: 0.625rem 1.25rem;\n background: var(--accent);\n color: #fff;\n border: 1px solid var(--accent);\n border-radius: 0;\n font-weight: 700;\n cursor: pointer;\n text-decoration: none;\n text-align: center;\n text-transform: uppercase;\n letter-spacing: 0.08em;\n font-size: 0.75rem;\n box-shadow:\n inset 1px 1px 0 rgba(255,255,255,0.15),\n inset -1px -1px 0 rgba(0,0,0,0.15),\n 2px 2px 0 rgba(0,0,0,0.1);\n transition: box-shadow 0.1s, transform 0.1s;\n }\n button:hover, .button:hover {\n box-shadow:\n inset 1px 1px 0 rgba(255,255,255,0.1),\n inset -1px -1px 0 rgba(0,0,0,0.15),\n 3px 3px 0 rgba(0,0,0,0.12);\n opacity: 1;\n }\n button:active, .button:active {\n transform: translate(1px, 1px);\n box-shadow: inset 1px 1px 3px rgba(0,0,0,0.25);\n }\n button:disabled, .button:disabled { opacity: 0.25; cursor: not-allowed; }\n\n button.secondary, .button.secondary {\n background: transparent;\n color: var(--text);\n border-color: var(--border-bright);\n box-shadow: 2px 2px 0 rgba(0,0,0,0.05);\n }\n button.secondary:hover, .button.secondary:hover {\n border-color: var(--accent);\n color: var(--accent);\n box-shadow: 2px 2px 0 rgba(0,0,0,0.1);\n }\n\n button.danger, .button.danger {\n background: var(--red);\n border-color: var(--red);\n color: #fff;\n box-shadow: 2px 2px 0 rgba(204,34,34,0.15);\n }\n button.danger:hover, .button.danger:hover {\n background: transparent;\n color: var(--red);\n }\n\n /* ============ Tables ============ */\n table { width: 100%; border-collapse: collapse; }\n\n thead { border-bottom: 1px solid var(--border-bright); }\n th {\n padding: 0.5rem 0.75rem; text-align: left;\n font-size: 0.6875rem; color: var(--text);\n font-weight: 700; text-transform: uppercase;\n letter-spacing: 0.1em;\n background: var(--bg-raised);\n }\n\n td {\n padding: 0.5rem 0.75rem;\n border-bottom: 1px solid var(--border);\n font-size: 0.75rem;\n font-variant-numeric: tabular-nums;\n }\n\n tbody tr:hover { background: var(--bg-raised); }\n\n /* ============ Code ============ */\n code, pre {\n font-family: var(--font);\n background: var(--bg-raised);\n padding: 0.125rem 0.375rem;\n border-radius: 0;\n font-size: 0.75rem;\n border: 1px solid var(--border);\n color: var(--text);\n }\n pre { padding: 1rem; overflow-x: auto; border: 1px solid var(--border-bright); }\n pre code { background: none; padding: 0; border: none; }\n\n /* ============ Login layout ============ */\n .login-container {\n min-height: 100vh;\n display: flex; align-items: center; justify-content: center;\n padding: 2rem;\n background: var(--bg);\n }\n .login-box { width: 100%; max-width: 420px; }\n\n .login-header { text-align: center; margin-bottom: 2rem; }\n .login-header h1 {\n font-size: 1.5rem; font-weight: 700; color: var(--text);\n letter-spacing: 0.15em; text-transform: uppercase;\n margin-bottom: 0.25rem;\n }\n .login-header p { color: var(--text-muted); font-size: 0.75rem; }\n\n /* ============ htmx loading ============ */\n .htmx-indicator { opacity: 0; transition: opacity 0.15s; }\n .htmx-request .htmx-indicator { opacity: 1; }\n .htmx-request.htmx-swapping { opacity: 0.3; pointer-events: none; }\n\n /* ============ Skeleton \u2014 blinking cursor ============ */\n .skeleton {\n background: var(--bg-raised);\n border: 1px solid var(--border);\n position: relative;\n overflow: hidden;\n }\n .skeleton::after {\n content: '';\n position: absolute; left: 0; top: 0;\n width: 2px; height: 100%;\n background: var(--text);\n animation: cursor-blink 0.8s step-end infinite;\n }\n @keyframes cursor-blink {\n 0%, 100% { opacity: 1; }\n 50% { opacity: 0; }\n }\n\n .skeleton-text { height: 1rem; margin-bottom: 0.5rem; }\n .skeleton-heading { height: 1.5rem; width: 60%; margin-bottom: 1rem; }\n\n /* ============ Utilities ============ */\n .text-center { text-align: center; }\n .text-right { text-align: right; }\n .text-muted { color: var(--text-muted); }\n .text-dim { color: var(--text-dim); }\n .text-success { color: var(--green); }\n .text-danger { color: var(--red); }\n .text-warning { color: var(--amber); }\n\n .mb-0 { margin-bottom: 0; }\n .mb-1 { margin-bottom: 0.5rem; }\n .mb-2 { margin-bottom: 1rem; }\n .mb-3 { margin-bottom: 1.5rem; }\n .mb-4 { margin-bottom: 2rem; }\n .mt-0 { margin-top: 0; }\n .mt-1 { margin-top: 0.5rem; }\n .mt-2 { margin-top: 1rem; }\n .mt-3 { margin-top: 1.5rem; }\n .mt-4 { margin-top: 2rem; }\n\n /* ============ Period selector ============ */\n .period-selector button {\n font-size: 0.625rem;\n padding: 0.2rem 0.5rem;\n }\n\n /* ============ Responsive ============ */\n @media (max-width: 768px) {\n html, body { font-size: 14px; }\n .dashboard-main { padding: 1rem; }\n .nav-container { padding: 0.5rem 1rem; }\n .dashboard-grid { grid-template-columns: 1fr; gap: 1rem; }\n .card-inner { padding: 1rem; }\n .card { margin-bottom: 1rem; }\n fieldset { padding: 1rem; margin-bottom: 1rem; }\n .stat-card .stat-value { font-size: 1.75rem; }\n table { font-size: 0.625rem; }\n th, td { padding: 0.375rem 0.5rem; }\n }\n\n @media (max-width: 480px) {\n .nav-container { flex-wrap: wrap; }\n .nav-app-id { margin-left: 0; width: 100%; order: 3; }\n }\n\n /* ============ Alerts ============ */\n .alert {\n padding: 0.75rem 1rem;\n border-radius: 0;\n margin-bottom: 1.5rem;\n border: 1px solid var(--border-bright);\n font-size: 0.75rem;\n background: var(--bg-card);\n }\n .alert::before { font-weight: 700; margin-right: 0.5rem; }\n\n .alert-info { border-color: var(--border-bright); color: var(--text); }\n .alert-info::before { content: '>'; color: var(--text); }\n\n .alert-success { border-color: var(--green); color: var(--green); }\n .alert-success::before { content: '[ok]'; }\n\n .alert-warning { border-color: var(--amber); color: var(--amber); }\n .alert-warning::before { content: '[!!]'; }\n\n .alert-danger { border-color: var(--red); color: var(--red); }\n .alert-danger::before { content: '[ERR]'; }\n\n /* ============ Badges ============ */\n .badge {\n display: inline-block;\n padding: 0.125rem 0.375rem;\n font-size: 0.625rem; font-weight: 700;\n border-radius: 0;\n text-transform: uppercase;\n letter-spacing: 0.05em;\n border: 1px solid;\n }\n .badge-success { color: var(--green); border-color: var(--green); background: transparent; }\n .badge-danger { color: var(--red); border-color: var(--red); background: transparent; }\n .badge-warning { color: var(--amber); border-color: var(--amber); background: transparent; }\n .badge-info { color: #fff; border-color: var(--accent); background: var(--accent); }\n\n /* ============ Sample data indicator ============ */\n .sample-banner {\n background: #fffbe6;\n border: 1px solid var(--amber);\n color: var(--amber);\n padding: 0.5rem 0.75rem;\n font-size: 0.6875rem;\n font-weight: 700;\n text-transform: uppercase;\n letter-spacing: 0.05em;\n margin-bottom: 1rem;\n }\n .sample-banner::before { content: '[demo]'; margin-right: 0.5rem; }\n .sample-tag {\n color: var(--amber);\n font-size: 0.625rem;\n font-weight: 700;\n letter-spacing: 0.05em;\n border: 1px solid var(--amber);\n padding: 0.0625rem 0.3rem;\n margin-left: 0.375rem;\n vertical-align: middle;\n background: #fffbe6;\n }\n\n /* ============ Empty state ============ */\n .empty-state { text-align: center; padding: 3rem 1rem; color: var(--text-muted); }\n .empty-state-icon { font-size: 1.5rem; margin-bottom: 0.75rem; color: var(--text); font-weight: 700; }\n .empty-state-text { font-size: 0.8125rem; margin-bottom: 0.25rem; }\n .empty-state-subtext { font-size: 0.6875rem; color: var(--text-dim); }\n\n /* ============ Auth pages ============ */\n .ascii-logo {\n display: block; text-align: center; margin-bottom: 2rem;\n color: var(--text); font-size: 0.55rem; line-height: 1.2;\n white-space: pre; font-weight: 400;\n text-decoration: none;\n }\n\n .badge-inline {\n display: inline-block; font-size: 0.5625rem; font-weight: 700;\n color: var(--text-muted); border: 1px solid var(--border-bright);\n border-radius: 0; padding: 0.1rem 0.4rem;\n margin-left: 0.5rem; vertical-align: middle;\n text-transform: uppercase; letter-spacing: 0.05em;\n }\n\n .divider {\n display: flex; align-items: center; gap: 0.75rem;\n color: var(--text-dim); font-size: 0.6875rem;\n margin: 1.5rem 0;\n text-transform: uppercase; letter-spacing: 0.1em;\n white-space: nowrap;\n }\n .divider::before, .divider::after {\n content: ''; flex: 1;\n height: 1px; background: var(--border-bright);\n }\n\n .credentials-box {\n background: var(--bg); border: 1px solid var(--accent-dim);\n padding: 1rem; margin-bottom: 1rem;\n font-size: 0.75rem; line-height: 1.8; word-break: break-all;\n }\n .credentials-box .label { color: var(--text-muted); }\n .credentials-box .value { color: var(--text); font-weight: 700; }\n\n .warning {\n background: rgba(184,122,0,0.06); border: 1px solid var(--amber);\n padding: 0.75rem; margin-bottom: 1rem;\n font-size: 0.7rem; color: var(--amber);\n }\n .warning::before { content: '[!!] '; font-weight: 700; }\n\n .error-message {\n color: var(--red); margin: 0 0 1rem 0; font-size: 0.75rem;\n padding: 0.5rem 0.75rem;\n border: 1px solid rgba(204,34,34,0.3);\n background: var(--bg);\n }\n .error-message::before { content: '[ERR] '; font-weight: 700; }\n\n .hint {\n font-size: 0.6875rem; color: var(--text-muted); line-height: 1.6;\n margin-top: 0.75rem;\n }\n .hint code {\n color: var(--text); background: var(--bg-raised);\n padding: 0.125rem 0.375rem; border: 1px solid var(--border);\n }\n\n .btn {\n display: block; width: 100%; text-align: center; text-decoration: none;\n }\n .btn-secondary {\n background: transparent; color: var(--text);\n border-color: var(--border-bright);\n box-shadow: 2px 2px 0 rgba(0,0,0,0.05);\n }\n .btn-secondary:hover {\n border-color: var(--accent); color: var(--accent);\n box-shadow: 2px 2px 0 rgba(0,0,0,0.1);\n }\n\n #create-result { display: none; }\n #create-result.show { display: block; }\n #create-btn.loading { opacity: 0.25; pointer-events: none; }\n\n /* ============ Landing page ============ */\n .landing-box { width: 100%; max-width: 580px; }\n .landing-box .ascii-logo { font-size: 0.75rem; margin-bottom: 1rem; }\n\n /* Landing page flows from top, not vertically centered like login */\n .login-container:has(.landing-box) {\n align-items: flex-start; padding-top: 4rem;\n }\n\n .landing-tagline {\n text-align: center; font-size: 0.8125rem;\n color: var(--text-muted); margin-bottom: 1.5rem;\n }\n\n .landing-links {\n display: flex; flex-wrap: wrap; justify-content: center;\n gap: 0.5rem; margin-bottom: 2rem;\n }\n .landing-link {\n font-size: 0.6875rem; color: var(--text);\n text-decoration: none; padding: 0.25rem 0.625rem;\n border: 1px solid var(--border-bright);\n transition: border-color 0.15s, background 0.15s;\n }\n .landing-link:hover {\n border-color: var(--accent); background: var(--bg-raised);\n }\n\n .landing-features {\n display: flex; flex-direction: column; gap: 0.75rem;\n margin-top: 1rem;\n }\n .landing-feature {\n display: flex; gap: 0.75rem; align-items: baseline;\n font-size: 0.75rem;\n }\n .landing-feature-label {\n font-weight: 700; color: var(--text);\n white-space: nowrap; min-width: 10rem;\n }\n .landing-feature-desc { color: var(--text-muted); }\n\n .landing-steps { display: flex; flex-direction: column; gap: 0.75rem; }\n .landing-step {\n display: flex; gap: 0.75rem; align-items: flex-start;\n font-size: 0.75rem; line-height: 1.6;\n }\n .landing-step-num {\n display: inline-flex; align-items: center; justify-content: center;\n min-width: 1.5rem; height: 1.5rem;\n border: 1px solid var(--border-bright);\n font-size: 0.6875rem; font-weight: 700; color: var(--text);\n flex-shrink: 0;\n }\n .landing-step-hint {\n display: block; font-size: 0.6875rem; color: var(--text-dim);\n margin-top: 0.125rem;\n }\n\n .landing-footer {\n text-align: center; padding: 2rem 0 0;\n font-size: 0.6875rem; color: var(--text-dim);\n }\n .landing-footer a {\n color: var(--text-muted); text-decoration: none;\n }\n .landing-footer a:hover { color: var(--text); }\n .landing-footer-sep { margin: 0 0.375rem; }\n\n /* ============ Scrollbar ============ */\n ::-webkit-scrollbar { width: 6px; height: 6px; }\n ::-webkit-scrollbar-track { background: var(--bg-raised); }\n ::-webkit-scrollbar-thumb { background: var(--border-bright); }\n ::-webkit-scrollbar-thumb:hover { background: var(--text-muted); }\n\n /* ============ Responsive (small screens) ============ */\n @media (max-width: 480px) {\n .ascii-logo { font-size: 0.4rem; }\n .landing-box .ascii-logo { font-size: 0.5rem; }\n .login-container { padding: 1rem; }\n .card-inner { padding: 1rem; }\n .landing-feature { flex-direction: column; gap: 0.125rem; }\n .landing-feature-label { min-width: auto; }\n }\n";
|
|
11
11
|
//# sourceMappingURL=styles.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"styles.d.ts","sourceRoot":"","sources":["../../src/dashboard/styles.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,eAAO,MAAM,aAAa,
|
|
1
|
+
{"version":3,"file":"styles.d.ts","sourceRoot":"","sources":["../../src/dashboard/styles.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,eAAO,MAAM,aAAa,yvqBAitBzB,CAAC"}
|