@net-protocol/profiles 0.1.5 → 0.1.7

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 CHANGED
@@ -12,8 +12,9 @@ yarn add @net-protocol/profiles
12
12
 
13
13
  ## Features
14
14
 
15
- - **Read profile data**: Profile picture, X username, bio, canvas content
15
+ - **Read profile data**: Profile picture, X username, bio, canvas content, custom CSS themes
16
16
  - **Write profile data**: Utilities to prepare Storage.put() transactions
17
+ - **CSS theming**: Demo themes, AI prompt generation, CSS sanitization, and theme selector definitions
17
18
  - **Efficient batch reads**: `useBasicUserProfileMetadata` batches multiple reads
18
19
  - **Built on net-storage**: Uses the Net Storage SDK for underlying storage operations
19
20
 
@@ -106,6 +107,7 @@ function UpdateProfile() {
106
107
  | Display Name | User-chosen display name | Max 25 characters |
107
108
  | Token Address | ERC-20 token that represents you | Valid EVM address (0x-prefixed) |
108
109
  | Canvas | Custom HTML profile page | For advanced customization |
110
+ | CSS Theme | Custom CSS for profile styling | Max 10KB, scoped under `.profile-themed` |
109
111
 
110
112
  ## Storage Keys
111
113
 
@@ -115,6 +117,7 @@ function UpdateProfile() {
115
117
  | `PROFILE_X_USERNAME_STORAGE_KEY` | X username (legacy, prefer metadata) | Plain string |
116
118
  | `PROFILE_METADATA_STORAGE_KEY` | Profile metadata JSON | `{ x_username: "handle", bio: "...", display_name: "...", token_address: "0x..." }` |
117
119
  | `PROFILE_CANVAS_STORAGE_KEY` | Custom HTML canvas | HTML string |
120
+ | `PROFILE_CSS_STORAGE_KEY` | Custom CSS theme | CSS string (max 10KB) |
118
121
 
119
122
  ## API Reference
120
123
 
@@ -123,6 +126,7 @@ function UpdateProfile() {
123
126
  - `useProfilePicture({ chainId, userAddress })` - Fetch profile picture URL
124
127
  - `useProfileXUsername({ chainId, userAddress })` - Fetch X username
125
128
  - `useProfileCanvas({ chainId, userAddress })` - Fetch canvas HTML
129
+ - `useProfileCSS({ chainId, userAddress })` - Fetch custom CSS theme
126
130
  - `useBasicUserProfileMetadata({ chainId, userAddress })` - Batch fetch picture, username, bio, display name, and token address
127
131
 
128
132
  ### Utilities (from `@net-protocol/profiles`)
@@ -140,6 +144,16 @@ function UpdateProfile() {
140
144
  - `isValidDisplayName(displayName)` - Validate display name format (max 25 chars, no control chars)
141
145
  - `getTokenAddressStorageArgs(tokenAddress)` - Prepare token address update args
142
146
  - `isValidTokenAddress(address)` - Validate EVM token address format
147
+ - `getProfileCSSStorageArgs(css)` - Prepare CSS theme update args
148
+ - `isValidCSS(css)` - Validate CSS (size limit, no script injection)
149
+ - `sanitizeCSS(css)` - Strip dangerous patterns (`<script>`, `javascript:`, `expression()`, `behavior:`, `@import`, `</style>`)
150
+
151
+ ### Theme Utilities (from `@net-protocol/profiles`)
152
+
153
+ - `THEME_SELECTORS` - Array of all themeable CSS selectors/variables with descriptions
154
+ - `DEMO_THEMES` - Built-in demo themes (use `buildCSSPrompt()` or CLI `--list-themes` to discover names)
155
+ - `buildCSSPrompt()` - Generate an AI prompt describing the full theming surface
156
+ - `MAX_CSS_SIZE` - Maximum CSS size in bytes (10KB)
143
157
 
144
158
  ## Dependencies
145
159
 
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