@net-protocol/profiles 0.1.5 → 0.1.6

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/dist/index.d.mts CHANGED
@@ -178,6 +178,14 @@ declare const MAX_CSS_SIZE: number;
178
178
  * ```
179
179
  */
180
180
  declare function getProfileCSSStorageArgs(cssContent: string): ProfileStorageArgs;
181
+ /**
182
+ * Sanitize user CSS to prevent injection attacks.
183
+ * - Strips </style> (which could break out of the style element during SSR)
184
+ * - Strips <script> tags
185
+ * - Removes javascript: URIs, expression(), behavior: (legacy IE vectors)
186
+ * - Removes @import rules (could load external resources / exfiltrate data)
187
+ */
188
+ declare function sanitizeCSS(css: string): string;
181
189
  /**
182
190
  * Validate CSS content
183
191
  * Returns true if valid (non-empty, within size limit, no script injection)
@@ -219,6 +227,10 @@ declare const THEME_SELECTORS: ThemeSelector[];
219
227
  /**
220
228
  * Demo themes that users can choose from as starting points.
221
229
  * Each is a complete CSS string ready to store on-chain.
230
+ *
231
+ * Themes include CSS variable overrides, @keyframes animations,
232
+ * backdrop-filter effects, and full component selector coverage
233
+ * including .profile-content overrides.
222
234
  */
223
235
  declare const DEMO_THEMES: Record<string, {
224
236
  name: string;
@@ -228,8 +240,12 @@ declare const DEMO_THEMES: Record<string, {
228
240
  * Build an AI prompt that describes the available theming surface.
229
241
  * Feed this to an LLM alongside a user's description to generate CSS.
230
242
  *
243
+ * The prompt includes per-selector documentation, animation guidance,
244
+ * !important rules for beating Tailwind utilities, and supported
245
+ * properties like backdrop-filter and box-shadow.
246
+ *
231
247
  * @returns A prompt string listing all available selectors and usage rules
232
248
  */
233
249
  declare function buildCSSPrompt(): string;
234
250
 
235
- export { DEMO_THEMES, MAX_CSS_SIZE, PROFILE_CANVAS_STORAGE_KEY, PROFILE_CANVAS_TOPIC, PROFILE_CSS_STORAGE_KEY, PROFILE_CSS_TOPIC, PROFILE_METADATA_STORAGE_KEY, PROFILE_METADATA_TOPIC, PROFILE_PICTURE_STORAGE_KEY, PROFILE_PICTURE_TOPIC, PROFILE_X_USERNAME_STORAGE_KEY, ProfileMetadata, ProfileStorageArgs, THEME_SELECTORS, type ThemeSelector, buildCSSPrompt, getBioStorageArgs, getBytesArgsForStorage, getDisplayNameStorageArgs, getProfileCSSStorageArgs, getProfileCanvasStorageArgs, getProfileMetadataStorageArgs, getProfilePictureStorageArgs, getTokenAddressStorageArgs, getValueArgForStorage, getXUsernameStorageArgs, isValidBio, isValidCSS, isValidDisplayName, isValidTokenAddress, isValidUrl, isValidXUsername, parseProfileMetadata };
251
+ export { DEMO_THEMES, MAX_CSS_SIZE, PROFILE_CANVAS_STORAGE_KEY, PROFILE_CANVAS_TOPIC, PROFILE_CSS_STORAGE_KEY, PROFILE_CSS_TOPIC, PROFILE_METADATA_STORAGE_KEY, PROFILE_METADATA_TOPIC, PROFILE_PICTURE_STORAGE_KEY, PROFILE_PICTURE_TOPIC, PROFILE_X_USERNAME_STORAGE_KEY, ProfileMetadata, ProfileStorageArgs, THEME_SELECTORS, type ThemeSelector, buildCSSPrompt, getBioStorageArgs, getBytesArgsForStorage, getDisplayNameStorageArgs, getProfileCSSStorageArgs, getProfileCanvasStorageArgs, getProfileMetadataStorageArgs, getProfilePictureStorageArgs, getTokenAddressStorageArgs, getValueArgForStorage, getXUsernameStorageArgs, isValidBio, isValidCSS, isValidDisplayName, isValidTokenAddress, isValidUrl, isValidXUsername, parseProfileMetadata, sanitizeCSS };
package/dist/index.d.ts CHANGED
@@ -178,6 +178,14 @@ declare const MAX_CSS_SIZE: number;
178
178
  * ```
179
179
  */
180
180
  declare function getProfileCSSStorageArgs(cssContent: string): ProfileStorageArgs;
181
+ /**
182
+ * Sanitize user CSS to prevent injection attacks.
183
+ * - Strips </style> (which could break out of the style element during SSR)
184
+ * - Strips <script> tags
185
+ * - Removes javascript: URIs, expression(), behavior: (legacy IE vectors)
186
+ * - Removes @import rules (could load external resources / exfiltrate data)
187
+ */
188
+ declare function sanitizeCSS(css: string): string;
181
189
  /**
182
190
  * Validate CSS content
183
191
  * Returns true if valid (non-empty, within size limit, no script injection)
@@ -219,6 +227,10 @@ declare const THEME_SELECTORS: ThemeSelector[];
219
227
  /**
220
228
  * Demo themes that users can choose from as starting points.
221
229
  * Each is a complete CSS string ready to store on-chain.
230
+ *
231
+ * Themes include CSS variable overrides, @keyframes animations,
232
+ * backdrop-filter effects, and full component selector coverage
233
+ * including .profile-content overrides.
222
234
  */
223
235
  declare const DEMO_THEMES: Record<string, {
224
236
  name: string;
@@ -228,8 +240,12 @@ declare const DEMO_THEMES: Record<string, {
228
240
  * Build an AI prompt that describes the available theming surface.
229
241
  * Feed this to an LLM alongside a user's description to generate CSS.
230
242
  *
243
+ * The prompt includes per-selector documentation, animation guidance,
244
+ * !important rules for beating Tailwind utilities, and supported
245
+ * properties like backdrop-filter and box-shadow.
246
+ *
231
247
  * @returns A prompt string listing all available selectors and usage rules
232
248
  */
233
249
  declare function buildCSSPrompt(): string;
234
250
 
235
- export { DEMO_THEMES, MAX_CSS_SIZE, PROFILE_CANVAS_STORAGE_KEY, PROFILE_CANVAS_TOPIC, PROFILE_CSS_STORAGE_KEY, PROFILE_CSS_TOPIC, PROFILE_METADATA_STORAGE_KEY, PROFILE_METADATA_TOPIC, PROFILE_PICTURE_STORAGE_KEY, PROFILE_PICTURE_TOPIC, PROFILE_X_USERNAME_STORAGE_KEY, ProfileMetadata, ProfileStorageArgs, THEME_SELECTORS, type ThemeSelector, buildCSSPrompt, getBioStorageArgs, getBytesArgsForStorage, getDisplayNameStorageArgs, getProfileCSSStorageArgs, getProfileCanvasStorageArgs, getProfileMetadataStorageArgs, getProfilePictureStorageArgs, getTokenAddressStorageArgs, getValueArgForStorage, getXUsernameStorageArgs, isValidBio, isValidCSS, isValidDisplayName, isValidTokenAddress, isValidUrl, isValidXUsername, parseProfileMetadata };
251
+ export { DEMO_THEMES, MAX_CSS_SIZE, PROFILE_CANVAS_STORAGE_KEY, PROFILE_CANVAS_TOPIC, PROFILE_CSS_STORAGE_KEY, PROFILE_CSS_TOPIC, PROFILE_METADATA_STORAGE_KEY, PROFILE_METADATA_TOPIC, PROFILE_PICTURE_STORAGE_KEY, PROFILE_PICTURE_TOPIC, PROFILE_X_USERNAME_STORAGE_KEY, ProfileMetadata, ProfileStorageArgs, THEME_SELECTORS, type ThemeSelector, buildCSSPrompt, getBioStorageArgs, getBytesArgsForStorage, getDisplayNameStorageArgs, getProfileCSSStorageArgs, getProfileCanvasStorageArgs, getProfileMetadataStorageArgs, getProfilePictureStorageArgs, getTokenAddressStorageArgs, getValueArgForStorage, getXUsernameStorageArgs, isValidBio, isValidCSS, isValidDisplayName, isValidTokenAddress, isValidUrl, isValidXUsername, parseProfileMetadata, sanitizeCSS };
package/dist/index.js CHANGED
@@ -131,6 +131,9 @@ function getProfileCSSStorageArgs(cssContent) {
131
131
  bytesValue
132
132
  };
133
133
  }
134
+ function sanitizeCSS(css) {
135
+ return css.replace(/<\/style>/gi, "").replace(/<script[\s\S]*?<\/script>/gi, "").replace(/javascript\s*:/gi, "").replace(/expression\s*\(/gi, "").replace(/behavior\s*:/gi, "").replace(/@import\b[^;]*;?/gi, "");
136
+ }
134
137
  function isValidCSS(css) {
135
138
  if (!css || css.trim().length === 0) return false;
136
139
  if (Buffer.byteLength(css, "utf-8") > MAX_CSS_SIZE) return false;
@@ -139,6 +142,8 @@ function isValidCSS(css) {
139
142
  if (lowerCSS.includes("behavior:")) return false;
140
143
  if (lowerCSS.includes("javascript:")) return false;
141
144
  if (/<script/i.test(css)) return false;
145
+ if (lowerCSS.includes("</style")) return false;
146
+ if (/@import\b/.test(lowerCSS)) return false;
142
147
  return true;
143
148
  }
144
149
 
@@ -220,118 +225,331 @@ var THEME_SELECTORS = [
220
225
  description: "Border radius (e.g. '0.5rem')",
221
226
  category: "variable"
222
227
  },
223
- // --- Layout selectors (may change with page restructuring) ---
228
+ // --- Layout selectors ---
224
229
  {
225
230
  selector: ".profile-themed",
226
231
  description: "Root wrapper for all themed profile content",
227
232
  category: "layout"
228
233
  },
229
- // --- Component selectors (may change with page restructuring) ---
234
+ // --- Component selectors ---
230
235
  {
231
- selector: ".profile-themed .profile-header",
232
- description: "Profile header area (name, picture, bio)",
236
+ selector: ".profile-header",
237
+ description: "Profile header card (name, avatar, bio, stat pills). Uses bg-gradient from-gray-900 to-gray-800, border-green-500",
233
238
  category: "component"
234
239
  },
235
240
  {
236
- selector: ".profile-themed .profile-tabs",
237
- description: "Tab navigation bar",
241
+ selector: ".profile-tabs",
242
+ description: "Tab navigation bar (Canvas, Posts, Feed, Activity). Uses bg-gray-800, border-gray-700. Active tab uses bg-green-600",
238
243
  category: "component"
239
244
  },
240
245
  {
241
- selector: ".profile-themed .profile-content",
242
- description: "Main content area below tabs",
246
+ selector: ".profile-content",
247
+ description: "Main content area below tabs (posts, canvas, feed, activity)",
243
248
  category: "component"
244
249
  }
245
250
  ];
246
251
  var DEMO_THEMES = {
247
- hotPink: {
248
- name: "Hot Pink Scene",
249
- css: `.profile-themed {
250
- --background: 320 80% 4%;
251
- --foreground: 320 20% 95%;
252
- --primary: 330 100% 60%;
252
+ checkerboard: {
253
+ name: "Checkerboard",
254
+ css: `@keyframes checker-scroll {
255
+ 0% { background-position: 0 0; }
256
+ 100% { background-position: 40px 40px; }
257
+ }
258
+ .profile-themed {
259
+ --primary: 0 0% 100%;
260
+ --primary-foreground: 0 0% 0%;
261
+ --card: 0 0% 5%;
262
+ --card-foreground: 0 0% 95%;
263
+ --border: 0 0% 30%;
264
+ --ring: 0 0% 100%;
265
+ --muted-foreground: 0 0% 60%;
266
+ --radius: 0px;
267
+ color: #e0e0e0;
268
+ background-image: repeating-conic-gradient(#333 0% 25%, #111 0% 50%);
269
+ background-size: 40px 40px;
270
+ animation: checker-scroll 3s linear infinite;
271
+ }
272
+ .profile-themed .profile-header {
273
+ background: rgba(0,0,0,0.4) !important;
274
+ background-color: rgba(0,0,0,0.4) !important;
275
+ background-image: none !important;
276
+ border-color: #fff !important;
277
+ border-width: 2px;
278
+ border-radius: 0 !important;
279
+ backdrop-filter: blur(4px);
280
+ }
281
+ .profile-themed .profile-tabs {
282
+ background: rgba(0,0,0,0.35) !important;
283
+ background-color: rgba(0,0,0,0.35) !important;
284
+ border-color: #555 !important;
285
+ border-radius: 0 !important;
286
+ backdrop-filter: blur(4px);
287
+ }
288
+ .profile-themed .profile-tabs button { color: #888 !important; }
289
+ .profile-themed .profile-tabs button.bg-green-600 {
290
+ background-color: rgba(255,255,255,0.9) !important;
291
+ color: #000 !important;
292
+ border-radius: 0 !important;
293
+ }
294
+ .profile-themed .profile-content .border-green-400 {
295
+ border-color: #555 !important;
296
+ border-radius: 0 !important;
297
+ background: rgba(0,0,0,0.35) !important;
298
+ background-color: rgba(0,0,0,0.35) !important;
299
+ backdrop-filter: blur(4px);
300
+ }
301
+ .profile-themed .profile-content .text-green-400 { color: #fff !important; }
302
+ .profile-themed .profile-content .text-green-300 { color: #ccc !important; }
303
+ .profile-themed .profile-content .text-white { color: #e0e0e0 !important; }
304
+ .profile-themed .profile-content .text-gray-500 { color: #666 !important; }
305
+ .profile-themed .profile-content .text-gray-400 { color: #888 !important; }`
306
+ },
307
+ neonPulse: {
308
+ name: "Neon Pulse",
309
+ css: `@keyframes neon-glow {
310
+ 0%, 100% { border-color: #ff00ff; box-shadow: 0 0 15px #ff00ff44; }
311
+ 33% { border-color: #00ffff; box-shadow: 0 0 15px #00ffff44; }
312
+ 66% { border-color: #ffff00; box-shadow: 0 0 15px #ffff0044; }
313
+ }
314
+ @keyframes hue-rotate {
315
+ 0% { filter: hue-rotate(0deg); }
316
+ 100% { filter: hue-rotate(360deg); }
317
+ }
318
+ .profile-themed {
319
+ --primary: 300 100% 60%;
253
320
  --primary-foreground: 0 0% 100%;
254
- --secondary: 280 60% 20%;
255
- --secondary-foreground: 320 20% 95%;
256
- --muted: 320 40% 12%;
257
- --muted-foreground: 320 20% 70%;
258
- --accent: 330 100% 60%;
259
- --accent-foreground: 0 0% 100%;
260
- --card: 320 60% 6%;
261
- --card-foreground: 320 20% 95%;
262
- --border: 330 60% 25%;
263
- --ring: 330 100% 60%;
264
- }`
321
+ --card: 260 80% 4%;
322
+ --card-foreground: 280 50% 92%;
323
+ --border: 300 100% 40%;
324
+ --ring: 300 100% 60%;
325
+ --muted-foreground: 280 30% 55%;
326
+ color: #e8d0ff;
327
+ }
328
+ .profile-themed .profile-header {
329
+ background: linear-gradient(135deg, #1a0030, #0d001a) !important;
330
+ border-width: 2px !important;
331
+ border-style: solid !important;
332
+ animation: neon-glow 4s ease-in-out infinite;
333
+ }
334
+ .profile-themed .profile-tabs {
335
+ background-color: #0d001a !important;
336
+ border-color: #6600aa !important;
337
+ }
338
+ .profile-themed .profile-tabs button { color: #aa66dd !important; }
339
+ .profile-themed .profile-tabs button.bg-green-600 {
340
+ background: linear-gradient(90deg, #ff00ff, #00ffff) !important;
341
+ color: #000 !important;
342
+ animation: hue-rotate 6s linear infinite;
343
+ }
344
+ .profile-themed .profile-content .border-green-400 {
345
+ border-width: 1px !important;
346
+ border-style: solid !important;
347
+ animation: neon-glow 4s ease-in-out infinite;
348
+ }
349
+ .profile-themed .profile-content .text-green-400 { color: #ff66ff !important; }
350
+ .profile-themed .profile-content .text-green-300 { color: #cc88ff !important; }
351
+ .profile-themed .profile-content .text-white { color: #e8d0ff !important; }
352
+ .profile-themed .profile-content .text-gray-500 { color: #6644aa !important; }
353
+ .profile-themed .profile-content .text-gray-400 { color: #9966cc !important; }`
265
354
  },
266
- midnightGrunge: {
267
- name: "Midnight Grunge",
268
- css: `.profile-themed {
269
- --background: 220 30% 3%;
270
- --foreground: 220 10% 80%;
271
- --primary: 45 90% 55%;
272
- --primary-foreground: 220 30% 5%;
273
- --secondary: 220 20% 12%;
274
- --secondary-foreground: 220 10% 80%;
275
- --muted: 220 20% 8%;
276
- --muted-foreground: 220 10% 50%;
277
- --accent: 45 90% 55%;
278
- --accent-foreground: 220 30% 5%;
279
- --card: 220 25% 5%;
280
- --card-foreground: 220 10% 80%;
281
- --border: 220 15% 15%;
282
- --ring: 45 90% 55%;
283
- }`
355
+ sunset: {
356
+ name: "Sunset",
357
+ css: `@keyframes sunset-shift {
358
+ 0%, 100% { background-position: 0% 50%; }
359
+ 50% { background-position: 100% 50%; }
360
+ }
361
+ .profile-themed {
362
+ --primary: 25 100% 55%;
363
+ --primary-foreground: 0 0% 100%;
364
+ --card: 15 60% 6%;
365
+ --card-foreground: 35 80% 90%;
366
+ --border: 20 80% 30%;
367
+ --ring: 25 100% 55%;
368
+ --muted-foreground: 20 40% 50%;
369
+ color: #fde4c8;
370
+ background: linear-gradient(135deg, #1a0a00, #2d0a1e, #0a0a2d, #1a0a00);
371
+ background-size: 400% 400%;
372
+ animation: sunset-shift 15s ease-in-out infinite;
373
+ }
374
+ .profile-themed .profile-header {
375
+ background: linear-gradient(135deg, #3d1200, #2d0a1e, #1a0033) !important;
376
+ border-color: #ff6600 !important;
377
+ border-width: 2px;
378
+ box-shadow: 0 0 30px #ff440022;
379
+ }
380
+ .profile-themed .profile-tabs {
381
+ background-color: #1a0a00dd !important;
382
+ border-color: #663300 !important;
383
+ }
384
+ .profile-themed .profile-tabs button { color: #cc8855 !important; }
385
+ .profile-themed .profile-tabs button.bg-green-600 {
386
+ background: linear-gradient(90deg, #ff4400, #ff8800) !important;
387
+ color: #fff !important;
388
+ }
389
+ .profile-themed .profile-content .border-green-400 {
390
+ border-color: #ff660044 !important;
391
+ box-shadow: 0 0 10px #ff440011;
392
+ }
393
+ .profile-themed .profile-content .text-green-400 { color: #ff8844 !important; }
394
+ .profile-themed .profile-content .text-green-300 { color: #ffaa66 !important; }
395
+ .profile-themed .profile-content .text-white { color: #fde4c8 !important; }
396
+ .profile-themed .profile-content .text-gray-500 { color: #8a6040 !important; }
397
+ .profile-themed .profile-content .text-gray-400 { color: #bb8866 !important; }`
284
398
  },
285
- ocean: {
286
- name: "Deep Ocean",
287
- css: `.profile-themed {
288
- --background: 200 60% 3%;
289
- --foreground: 190 20% 90%;
290
- --primary: 190 80% 50%;
291
- --primary-foreground: 200 60% 5%;
292
- --secondary: 210 40% 15%;
293
- --secondary-foreground: 190 20% 90%;
294
- --muted: 200 40% 8%;
295
- --muted-foreground: 190 20% 55%;
296
- --accent: 170 70% 45%;
297
- --accent-foreground: 200 60% 5%;
298
- --card: 200 50% 5%;
299
- --card-foreground: 190 20% 90%;
300
- --border: 200 30% 18%;
301
- --ring: 190 80% 50%;
302
- }`
399
+ psychedelic: {
400
+ name: "Dreamscape",
401
+ css: `@keyframes dreamDrift {
402
+ 0% { background-position: 0% 50%; }
403
+ 25% { background-position: 100% 30%; }
404
+ 50% { background-position: 80% 100%; }
405
+ 75% { background-position: 20% 60%; }
406
+ 100% { background-position: 0% 50%; }
407
+ }
408
+ @keyframes floatCircles {
409
+ 0% { transform: translate(0, 0) rotate(0deg); }
410
+ 33% { transform: translate(20px, -30px) rotate(120deg); }
411
+ 66% { transform: translate(-15px, 20px) rotate(240deg); }
412
+ 100% { transform: translate(0, 0) rotate(360deg); }
413
+ }
414
+ @keyframes flowBorder {
415
+ 0% { background-position: 0 0, 0% 0%; }
416
+ 100% { background-position: 0 0, 300% 300%; }
417
+ }
418
+ @keyframes glowPulse {
419
+ 0% { box-shadow: 0 0 15px hsl(270 60% 70% / 0.3), 0 0 30px hsl(270 60% 65% / 0.1); }
420
+ 50% { box-shadow: 0 0 25px hsl(220 60% 70% / 0.4), 0 0 50px hsl(220 50% 65% / 0.15); }
421
+ 100% { box-shadow: 0 0 15px hsl(270 60% 70% / 0.3), 0 0 30px hsl(270 60% 65% / 0.1); }
422
+ }
423
+ @keyframes tabGlow {
424
+ 0% { background-position: 0% 50%; }
425
+ 50% { background-position: 100% 50%; }
426
+ 100% { background-position: 0% 50%; }
427
+ }
428
+ .profile-themed {
429
+ --background: 260 30% 6%;
430
+ --foreground: 250 30% 90%;
431
+ --primary: 270 60% 72%;
432
+ --primary-foreground: 260 30% 10%;
433
+ --secondary: 220 50% 65%;
434
+ --secondary-foreground: 260 30% 10%;
435
+ --muted: 260 20% 15%;
436
+ --muted-foreground: 250 25% 65%;
437
+ --accent: 200 50% 70%;
438
+ --accent-foreground: 260 30% 10%;
439
+ --card: 260 25% 10%;
440
+ --card-foreground: 250 30% 90%;
441
+ --border: 270 40% 50%;
442
+ --ring: 270 60% 72%;
443
+ --radius: 0.75rem;
444
+ color: hsl(250 30% 90%) !important;
445
+ position: relative;
446
+ overflow: hidden;
447
+ background-image: linear-gradient(-45deg, hsl(260 30% 8%), hsl(270 50% 30%), hsl(220 40% 25%), hsl(280 40% 20%), hsl(240 30% 12%)) !important;
448
+ background-size: 400% 400%;
449
+ animation: dreamDrift 20s ease-in-out infinite;
450
+ }
451
+ .profile-themed::after {
452
+ content: "";
453
+ position: absolute;
454
+ top: 40px;
455
+ left: 30px;
456
+ width: 80px;
457
+ height: 80px;
458
+ border-radius: 50%;
459
+ background: hsl(270 60% 80% / 0.12);
460
+ box-shadow:
461
+ 200px 100px 0 40px hsl(220 60% 80% / 0.1),
462
+ 50px 300px 0 60px hsl(280 50% 75% / 0.1),
463
+ 320px 400px 0 35px hsl(200 50% 80% / 0.12),
464
+ 150px 550px 0 50px hsl(260 50% 75% / 0.1);
465
+ filter: blur(10px);
466
+ animation: floatCircles 30s ease-in-out infinite;
467
+ z-index: 2;
468
+ pointer-events: none;
469
+ }
470
+ .profile-themed .profile-header {
471
+ background: hsl(260 25% 10% / 0.75) !important;
472
+ background-image: none !important;
473
+ border-color: hsl(270 40% 50% / 0.3) !important;
474
+ backdrop-filter: blur(30px) saturate(140%);
475
+ box-shadow: 0 0 30px hsl(270 50% 60% / 0.15);
476
+ }
477
+ .profile-themed .profile-tabs {
478
+ background-color: hsl(260 25% 12% / 0.8) !important;
479
+ border-color: hsl(270 40% 50% / 0.3) !important;
480
+ backdrop-filter: blur(20px) saturate(140%);
481
+ }
482
+ .profile-themed .profile-tabs button {
483
+ color: hsl(250 25% 65%) !important;
484
+ }
485
+ .profile-themed .profile-tabs button.bg-green-600 {
486
+ background-image: linear-gradient(90deg, hsl(270 60% 65%), hsl(220 50% 65%), hsl(280 50% 70%)) !important;
487
+ background-size: 300% 300%;
488
+ animation: tabGlow 8s ease infinite;
489
+ color: hsl(0 0% 100%) !important;
490
+ }
491
+ .profile-themed .profile-content {
492
+ background: hsl(260 25% 8% / 0.6) !important;
493
+ }
494
+ .profile-themed .profile-content .border-green-400 {
495
+ border: 2px solid transparent !important;
496
+ background-image:
497
+ linear-gradient(hsl(260 25% 12% / 0.85), hsl(260 25% 12% / 0.85)),
498
+ linear-gradient(135deg, hsl(270 60% 65%), hsl(220 50% 65%), hsl(200 50% 70%), hsl(280 50% 70%), hsl(270 60% 65%)) !important;
499
+ background-origin: border-box !important;
500
+ background-clip: padding-box, border-box !important;
501
+ background-size: 100% 100%, 400% 400% !important;
502
+ backdrop-filter: blur(15px) saturate(140%);
503
+ animation: flowBorder 6s linear infinite, glowPulse 4s ease-in-out infinite;
504
+ }
505
+ .profile-themed .profile-content .text-green-400 {
506
+ color: hsl(270 60% 75%) !important;
507
+ text-shadow: 0 0 12px hsl(270 60% 70% / 0.4);
508
+ }
509
+ .profile-themed .profile-content .text-green-300 { color: hsl(220 50% 75%) !important; }
510
+ .profile-themed .profile-content .text-white { color: hsl(250 30% 90%) !important; }
511
+ .profile-themed .profile-content .text-gray-500 { color: hsl(250 20% 50%) !important; }
512
+ .profile-themed .profile-content .text-gray-400 { color: hsl(250 25% 65%) !important; }`
303
513
  }
304
514
  };
305
515
  function buildCSSPrompt() {
306
516
  const variableLines = THEME_SELECTORS.filter(
307
517
  (s) => s.category === "variable"
308
518
  ).map((s) => ` ${s.selector}: ${s.description}`).join("\n");
309
- const layoutLines = THEME_SELECTORS.filter((s) => s.category === "layout").map((s) => ` ${s.selector} \u2014 ${s.description}`).join("\n");
310
- const componentLines = THEME_SELECTORS.filter(
311
- (s) => s.category === "component"
312
- ).map((s) => ` ${s.selector} \u2014 ${s.description}`).join("\n");
313
519
  return `You are a CSS theme generator for a user profile page.
314
- All styles MUST be scoped under the .profile-themed wrapper class.
520
+ All styles MUST be scoped under .profile-themed.
315
521
 
316
- ## CSS Variables (stable \u2014 preferred for theming)
317
- These use HSL values WITHOUT the hsl() wrapper (e.g. "210 40% 98%"):
522
+ ## CSS Variables (set inside .profile-themed { ... })
523
+ HSL values WITHOUT hsl() wrapper (e.g. "210 40% 98%"):
318
524
  ${variableLines}
319
525
 
320
- ## Layout Selectors (may change across site updates)
321
- ${layoutLines}
322
-
323
- ## Component Selectors (may change across site updates)
324
- ${componentLines}
526
+ ## Component Selectors
527
+ .profile-themed \u2014 root wrapper; set CSS variables, \`color\`, and optionally background/background-image/animation for full-page effects
528
+ .profile-themed .profile-header \u2014 header card; override background, background-image: none, border-color
529
+ .profile-themed .profile-tabs \u2014 tab bar; override background-color, border-color
530
+ .profile-themed .profile-tabs button \u2014 inactive tab text color
531
+ .profile-themed .profile-tabs button.bg-green-600 \u2014 active tab background + color
532
+ .profile-themed .profile-content \u2014 content area below tabs
533
+ .profile-themed .profile-content .border-green-400 \u2014 post card borders + background
534
+ .profile-themed .profile-content .text-green-400 \u2014 post links/usernames
535
+ .profile-themed .profile-content .text-green-300 \u2014 secondary links
536
+ .profile-themed .profile-content .text-white \u2014 post body text
537
+ .profile-themed .profile-content .text-gray-500 \u2014 timestamps
538
+ .profile-themed .profile-content .text-gray-400 \u2014 secondary text
325
539
 
326
540
  ## Rules
327
541
  1. All selectors MUST start with .profile-themed
328
- 2. CSS variables go inside .profile-themed { ... } as custom properties
329
- 3. Use only valid CSS \u2014 no JavaScript, no expressions, no imports
330
- 4. Keep the output under 10KB
331
- 5. Prefer CSS variables over direct selector styling for durability
332
- 6. HSL values are bare numbers: "210 40% 98%" not "hsl(210, 40%, 98%)"
542
+ 2. Use !important on color, background, background-color, background-image, border-color overrides (needed to beat Tailwind utilities)
543
+ 3. Set background-image: none !important on .profile-header to clear its default gradient
544
+ 4. Set \`color\` on .profile-themed for inherited text color
545
+ 5. @keyframes animations are encouraged \u2014 use for backgrounds, borders, glows
546
+ 6. IMPORTANT: Do NOT use \`background\` shorthand with !important if you animate background-position \u2014 the shorthand locks background-position with !important and animations cannot override it. Use \`background-image\` instead.
547
+ 7. backdrop-filter, box-shadow, and gradients are supported
548
+ 8. Use valid CSS only \u2014 no JS, no expressions, no imports
549
+ 9. Keep under 10KB
550
+ 10. HSL values are bare: "210 40% 98%" not "hsl(210, 40%, 98%)"
333
551
 
334
- Given a user description, output ONLY the CSS (no explanation, no markdown fences).`;
552
+ Output ONLY the CSS.`;
335
553
  }
336
554
 
337
555
  Object.defineProperty(exports, "STORAGE_CONTRACT", {
@@ -368,5 +586,6 @@ exports.isValidTokenAddress = isValidTokenAddress;
368
586
  exports.isValidUrl = isValidUrl;
369
587
  exports.isValidXUsername = isValidXUsername;
370
588
  exports.parseProfileMetadata = parseProfileMetadata;
589
+ exports.sanitizeCSS = sanitizeCSS;
371
590
  //# sourceMappingURL=index.js.map
372
591
  //# sourceMappingURL=index.js.map