@commonpub/layer 0.4.11 → 0.4.13

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@commonpub/layer",
3
- "version": "0.4.11",
3
+ "version": "0.4.13",
4
4
  "type": "module",
5
5
  "main": "./nuxt.config.ts",
6
6
  "files": [
@@ -51,14 +51,14 @@
51
51
  "vue-router": "^4.3.0",
52
52
  "zod": "^4.3.6",
53
53
  "@commonpub/auth": "0.5.0",
54
- "@commonpub/editor": "0.5.0",
55
54
  "@commonpub/learning": "0.5.0",
56
- "@commonpub/schema": "0.8.14",
55
+ "@commonpub/schema": "0.8.15",
56
+ "@commonpub/editor": "0.5.0",
57
57
  "@commonpub/config": "0.8.0",
58
- "@commonpub/explainer": "0.5.3",
59
58
  "@commonpub/docs": "0.5.2",
59
+ "@commonpub/explainer": "0.5.3",
60
+ "@commonpub/server": "2.23.1",
60
61
  "@commonpub/ui": "0.8.4",
61
- "@commonpub/server": "2.23.0",
62
62
  "@commonpub/protocol": "0.9.5"
63
63
  },
64
64
  "devDependencies": {
@@ -30,14 +30,18 @@ const form = ref({
30
30
  bannerUrl: '',
31
31
  });
32
32
 
33
- const skills = ref<Array<{ name: string; proficiency: number }>>([]);
33
+ const skills = ref<string[]>([]);
34
34
  const socialLinks = ref({
35
35
  github: '',
36
36
  twitter: '',
37
37
  linkedin: '',
38
- website: '',
38
+ youtube: '',
39
+ instagram: '',
40
+ mastodon: '',
41
+ discord: '',
39
42
  });
40
- const experience = ref<Array<{ id: string; title: string; company: string; startDate: string; endDate: string; description: string }>>([]);
43
+ const pronouns = ref('');
44
+ const experience = ref<Array<{ title: string; company: string; startDate: string; endDate: string; description: string }>>([]);
41
45
 
42
46
  const emailNotifications = ref<{
43
47
  digest: 'daily' | 'weekly' | 'none';
@@ -75,19 +79,28 @@ if (profile.value) {
75
79
  form.value.bannerUrl = p.bannerUrl || '';
76
80
 
77
81
  if (Array.isArray(p.skills)) {
78
- skills.value = p.skills.map((s) =>
79
- typeof s === 'string' ? { name: s, proficiency: 3 } : s,
80
- );
82
+ skills.value = p.skills.filter((s): s is string => typeof s === 'string');
81
83
  }
84
+ pronouns.value = p.pronouns || '';
82
85
  if (p.socialLinks) {
83
- socialLinks.value.github = p.socialLinks.github || '';
84
- socialLinks.value.twitter = p.socialLinks.twitter || '';
85
- socialLinks.value.linkedin = p.socialLinks.linkedin || '';
86
- socialLinks.value.website = (p.socialLinks as Record<string, string | undefined>).website || '';
86
+ const sl = p.socialLinks as Record<string, string | undefined>;
87
+ socialLinks.value.github = sl.github || '';
88
+ socialLinks.value.twitter = sl.twitter || '';
89
+ socialLinks.value.linkedin = sl.linkedin || '';
90
+ socialLinks.value.youtube = sl.youtube || '';
91
+ socialLinks.value.instagram = sl.instagram || '';
92
+ socialLinks.value.mastodon = sl.mastodon || '';
93
+ socialLinks.value.discord = sl.discord || '';
87
94
  }
88
95
  const profileRecord = p as Record<string, unknown>;
89
96
  if (Array.isArray(profileRecord.experience)) {
90
- experience.value = (profileRecord.experience as Array<Record<string, unknown>>).map((e) => ({ ...e }) as typeof experience.value[number]);
97
+ experience.value = (profileRecord.experience as Array<Record<string, unknown>>).map((e) => ({
98
+ title: String(e.title || ''),
99
+ company: String(e.company || ''),
100
+ startDate: String(e.startDate || ''),
101
+ endDate: String(e.endDate || ''),
102
+ description: String(e.description || ''),
103
+ }));
91
104
  }
92
105
  if (profileRecord.emailNotifications && typeof profileRecord.emailNotifications === 'object') {
93
106
  const en = profileRecord.emailNotifications as Record<string, unknown>;
@@ -104,25 +117,20 @@ if (profile.value) {
104
117
  // Watch for form changes AFTER initial data is loaded (nextTick avoids false positive)
105
118
  onMounted(() => {
106
119
  nextTick(() => {
107
- watch([form, skills, socialLinks, experience, emailNotifications], () => { isDirty.value = true; }, { deep: true });
120
+ watch([form, skills, pronouns, socialLinks, experience, emailNotifications], () => { isDirty.value = true; }, { deep: true });
108
121
  });
109
122
  });
110
123
 
111
124
  function addSkill(): void {
112
- skills.value.push({ name: '', proficiency: 50 });
125
+ skills.value.push('');
113
126
  }
114
127
 
115
128
  function removeSkill(index: number): void {
116
129
  skills.value.splice(index, 1);
117
130
  }
118
131
 
119
- function generateId(): string {
120
- return `exp-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`;
121
- }
122
-
123
132
  function addExperience(): void {
124
133
  experience.value.push({
125
- id: generateId(),
126
134
  title: '',
127
135
  company: '',
128
136
  startDate: '',
@@ -170,8 +178,9 @@ async function handleSave(): Promise<void> {
170
178
  method: 'PUT',
171
179
  body: {
172
180
  ...form.value,
173
- skills: skills.value.filter((s) => s.name.trim()).map((s) => s.name),
181
+ skills: skills.value.filter((s) => s.trim()),
174
182
  experience: experience.value.filter((e) => e.title.trim()),
183
+ pronouns: pronouns.value || undefined,
175
184
  socialLinks: socialLinks.value,
176
185
  ...(emailNotificationsEnabled.value ? { emailNotifications: emailNotifications.value } : {}),
177
186
  },
@@ -330,39 +339,42 @@ async function handleSave(): Promise<void> {
330
339
  </div>
331
340
  </div>
332
341
 
342
+ <!-- Pronouns -->
343
+ <div class="cpub-form-section">
344
+ <span class="cpub-form-section-label">Identity</span>
345
+
346
+ <div class="cpub-form-group">
347
+ <label for="pronouns" class="cpub-form-label">Pronouns</label>
348
+ <input
349
+ id="pronouns"
350
+ v-model="pronouns"
351
+ type="text"
352
+ class="cpub-input"
353
+ placeholder="e.g., they/them, she/her, he/him"
354
+ />
355
+ </div>
356
+ </div>
357
+
333
358
  <!-- Skills -->
334
359
  <div class="cpub-form-section">
335
360
  <span class="cpub-form-section-label">Skills</span>
336
361
 
337
362
  <div
338
- v-for="(skill, index) in skills"
363
+ v-for="(_skill, index) in skills"
339
364
  :key="index"
340
365
  class="cpub-skill-row"
341
366
  >
342
- <div class="cpub-skill-name">
343
- <input
344
- v-model="skill.name"
345
- type="text"
346
- class="cpub-input"
347
- placeholder="Skill name"
348
- :aria-label="`Skill ${index + 1} name`"
349
- />
350
- </div>
351
- <div class="cpub-skill-slider">
352
- <input
353
- v-model.number="skill.proficiency"
354
- type="range"
355
- min="0"
356
- max="100"
357
- class="cpub-range"
358
- :aria-label="`Skill ${index + 1} proficiency`"
359
- />
360
- <span class="cpub-skill-value">{{ skill.proficiency }}%</span>
361
- </div>
367
+ <input
368
+ v-model="skills[index]"
369
+ type="text"
370
+ class="cpub-input"
371
+ placeholder="Skill name"
372
+ :aria-label="`Skill ${index + 1}`"
373
+ />
362
374
  <button
363
375
  type="button"
364
376
  class="cpub-btn-icon cpub-btn-danger"
365
- :aria-label="`Remove skill ${skill.name || index + 1}`"
377
+ :aria-label="`Remove skill ${skills[index] || index + 1}`"
366
378
  @click="removeSkill(index)"
367
379
  >
368
380
  <i class="fa-solid fa-xmark" aria-hidden="true"></i>
@@ -417,15 +429,25 @@ async function handleSave(): Promise<void> {
417
429
  </div>
418
430
 
419
431
  <div class="cpub-form-group">
420
- <label for="social-website" class="cpub-form-label">Website URL</label>
421
- <input
422
- id="social-website"
423
- v-model="socialLinks.website"
424
- type="url"
425
- class="cpub-input"
426
- placeholder="https://..."
427
- />
432
+ <label for="social-youtube" class="cpub-form-label">YouTube</label>
433
+ <input id="social-youtube" v-model="socialLinks.youtube" type="url" class="cpub-input" placeholder="https://youtube.com/@channel" />
434
+ </div>
435
+
436
+ <div class="cpub-form-group">
437
+ <label for="social-instagram" class="cpub-form-label">Instagram</label>
438
+ <input id="social-instagram" v-model="socialLinks.instagram" type="url" class="cpub-input" placeholder="https://instagram.com/username" />
439
+ </div>
440
+
441
+ <div class="cpub-form-group">
442
+ <label for="social-mastodon" class="cpub-form-label">Mastodon</label>
443
+ <input id="social-mastodon" v-model="socialLinks.mastodon" type="url" class="cpub-input" placeholder="https://mastodon.social/@username" />
444
+ </div>
445
+
446
+ <div class="cpub-form-group">
447
+ <label for="social-discord" class="cpub-form-label">Discord</label>
448
+ <input id="social-discord" v-model="socialLinks.discord" type="url" class="cpub-input" placeholder="https://discord.gg/invite" />
428
449
  </div>
450
+
429
451
  </div>
430
452
 
431
453
  <!-- Experience -->
@@ -434,7 +456,7 @@ async function handleSave(): Promise<void> {
434
456
 
435
457
  <div
436
458
  v-for="(entry, index) in experience"
437
- :key="entry.id"
459
+ :key="index"
438
460
  class="cpub-experience-card"
439
461
  >
440
462
  <div class="cpub-experience-header">
@@ -451,9 +473,9 @@ async function handleSave(): Promise<void> {
451
473
 
452
474
  <div class="cpub-experience-fields">
453
475
  <div class="cpub-form-group">
454
- <label :for="`exp-title-${entry.id}`" class="cpub-form-label">Title</label>
476
+ <label :for="`exp-title-${index}`" class="cpub-form-label">Title</label>
455
477
  <input
456
- :id="`exp-title-${entry.id}`"
478
+ :id="`exp-title-${index}`"
457
479
  v-model="entry.title"
458
480
  type="text"
459
481
  class="cpub-input"
@@ -462,9 +484,9 @@ async function handleSave(): Promise<void> {
462
484
  </div>
463
485
 
464
486
  <div class="cpub-form-group">
465
- <label :for="`exp-company-${entry.id}`" class="cpub-form-label">Company</label>
487
+ <label :for="`exp-company-${index}`" class="cpub-form-label">Company</label>
466
488
  <input
467
- :id="`exp-company-${entry.id}`"
489
+ :id="`exp-company-${index}`"
468
490
  v-model="entry.company"
469
491
  type="text"
470
492
  class="cpub-input"
@@ -474,18 +496,18 @@ async function handleSave(): Promise<void> {
474
496
 
475
497
  <div class="cpub-experience-dates">
476
498
  <div class="cpub-form-group">
477
- <label :for="`exp-start-${entry.id}`" class="cpub-form-label">Start Date</label>
499
+ <label :for="`exp-start-${index}`" class="cpub-form-label">Start Date</label>
478
500
  <input
479
- :id="`exp-start-${entry.id}`"
501
+ :id="`exp-start-${index}`"
480
502
  v-model="entry.startDate"
481
503
  type="month"
482
504
  class="cpub-input"
483
505
  />
484
506
  </div>
485
507
  <div class="cpub-form-group">
486
- <label :for="`exp-end-${entry.id}`" class="cpub-form-label">End Date</label>
508
+ <label :for="`exp-end-${index}`" class="cpub-form-label">End Date</label>
487
509
  <input
488
- :id="`exp-end-${entry.id}`"
510
+ :id="`exp-end-${index}`"
489
511
  v-model="entry.endDate"
490
512
  type="month"
491
513
  class="cpub-input"
@@ -495,9 +517,9 @@ async function handleSave(): Promise<void> {
495
517
  </div>
496
518
 
497
519
  <div class="cpub-form-group">
498
- <label :for="`exp-desc-${entry.id}`" class="cpub-form-label">Description</label>
520
+ <label :for="`exp-desc-${index}`" class="cpub-form-label">Description</label>
499
521
  <textarea
500
- :id="`exp-desc-${entry.id}`"
522
+ :id="`exp-desc-${index}`"
501
523
  v-model="entry.description"
502
524
  class="cpub-textarea"
503
525
  rows="3"
@@ -749,58 +771,6 @@ async function handleSave(): Promise<void> {
749
771
  margin-bottom: var(--space-3);
750
772
  }
751
773
 
752
- .cpub-skill-name {
753
- flex: 1;
754
- min-width: 0;
755
- }
756
-
757
- .cpub-skill-slider {
758
- display: flex;
759
- align-items: center;
760
- gap: var(--space-2);
761
- width: 180px;
762
- flex-shrink: 0;
763
- }
764
-
765
- .cpub-range {
766
- flex: 1;
767
- appearance: none;
768
- height: 4px;
769
- background: var(--border2);
770
- outline: none;
771
- cursor: pointer;
772
- }
773
-
774
- .cpub-range::-webkit-slider-thumb {
775
- appearance: none;
776
- width: 14px;
777
- height: 14px;
778
- background: var(--accent);
779
- border: var(--border-width-default) solid var(--accent);
780
- cursor: pointer;
781
- }
782
-
783
- .cpub-range::-moz-range-thumb {
784
- width: 14px;
785
- height: 14px;
786
- background: var(--accent);
787
- border: var(--border-width-default) solid var(--accent);
788
- cursor: pointer;
789
- }
790
-
791
- .cpub-range:focus-visible::-webkit-slider-thumb {
792
- outline: 2px solid var(--accent);
793
- outline-offset: 2px;
794
- }
795
-
796
- .cpub-skill-value {
797
- font-family: var(--font-mono);
798
- font-size: var(--text-xs);
799
- color: var(--text-dim);
800
- min-width: 36px;
801
- text-align: right;
802
- }
803
-
804
774
  /* ─── Buttons ─── */
805
775
  .cpub-btn-icon {
806
776
  width: 32px;
@@ -979,12 +949,8 @@ async function handleSave(): Promise<void> {
979
949
 
980
950
  @media (max-width: 768px) {
981
951
  .cpub-settings-form { padding: 0 var(--space-1); }
982
- .cpub-skill-slider { width: 120px; }
983
952
  .cpub-experience-dates { grid-template-columns: 1fr; }
984
953
  .cpub-banner-upload { height: 100px; }
985
954
  }
986
955
 
987
- @media (max-width: 480px) {
988
- .cpub-skill-slider { width: 80px; }
989
- }
990
956
  </style>
@@ -6,6 +6,8 @@ useSeoMeta({
6
6
  title: `${username} — ${useSiteName()}`,
7
7
  ogTitle: `${username} — ${useSiteName()}`,
8
8
  ogImage: '/og-default.png',
9
+ ogType: 'profile',
10
+ twitterCard: 'summary',
9
11
  });
10
12
 
11
13
  const { explainers: explainersEnabled, learning: learningEnabled, federation: federationEnabled } = useFeatures();
@@ -370,6 +372,25 @@ async function handleReport(): Promise<void> {
370
372
  <p class="cpub-empty-state-title">No skills listed yet</p>
371
373
  </div>
372
374
  </div>
375
+
376
+ <!-- Experience -->
377
+ <div v-if="p.experience?.length" class="cpub-experience-section">
378
+ <div class="cpub-sec-head">
379
+ <h2><i class="fa-solid fa-briefcase" style="color: var(--purple); margin-right: 6px"></i>Experience</h2>
380
+ </div>
381
+ <div class="cpub-experience-list">
382
+ <div v-for="(exp, idx) in p.experience" :key="idx" class="cpub-experience-item">
383
+ <div class="cpub-exp-header">
384
+ <strong>{{ exp.title }}</strong>
385
+ <span v-if="exp.company" class="cpub-exp-company">{{ exp.company }}</span>
386
+ </div>
387
+ <div v-if="exp.startDate" class="cpub-exp-dates">
388
+ {{ exp.startDate }}{{ exp.endDate ? ` — ${exp.endDate}` : ' — Present' }}
389
+ </div>
390
+ <p v-if="exp.description" class="cpub-exp-desc">{{ exp.description }}</p>
391
+ </div>
392
+ </div>
393
+ </div>
373
394
  </div>
374
395
 
375
396
  <!-- Sidebar -->
@@ -709,6 +730,20 @@ async function handleReport(): Promise<void> {
709
730
  margin-bottom: 8px;
710
731
  }
711
732
 
733
+ /* Experience */
734
+ .cpub-experience-section { margin-top: 24px; }
735
+ .cpub-experience-list { display: flex; flex-direction: column; gap: 16px; }
736
+ .cpub-experience-item {
737
+ padding: 12px 16px;
738
+ border: var(--border-width-default) solid var(--border2);
739
+ background: var(--surface);
740
+ }
741
+ .cpub-exp-header { display: flex; flex-direction: column; gap: 2px; }
742
+ .cpub-exp-header strong { font-size: var(--text-sm); }
743
+ .cpub-exp-company { font-size: var(--text-xs); color: var(--text-dim); }
744
+ .cpub-exp-dates { font-size: var(--text-xs); color: var(--text-faint); font-family: var(--font-mono); margin-top: 4px; }
745
+ .cpub-exp-desc { font-size: var(--text-sm); color: var(--text-dim); margin-top: 8px; line-height: var(--leading-normal); }
746
+
712
747
  /* About grid */
713
748
  .cpub-about-grid {
714
749
  display: grid;