@jx3box/jx3box-editor 3.2.5 → 3.2.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.
Files changed (33) hide show
  1. package/.storybook/main.js +70 -0
  2. package/.storybook/middleware.js +73 -0
  3. package/.storybook/preview.js +90 -0
  4. package/package.json +9 -1
  5. package/src/Article.vue +6 -1
  6. package/src/ArticleMarkdown.vue +1 -1
  7. package/src/assets/css/markdown/markdown-article.less +13 -13
  8. package/src/assets/css/markdown/markdown-editor.less +11 -11
  9. package/src/components/Author.vue +312 -52
  10. package/src/components/QRcode.vue +1 -1
  11. package/src/service/cms.js +13 -1
  12. package/src/storybook/storybook-vars.less +1 -0
  13. package/src/storybook/storybook.helpers.js +154 -0
  14. package/stories/components/Author.stories.js +36 -0
  15. package/stories/components/Avatar.stories.js +55 -0
  16. package/stories/components/Combo.stories.js +37 -0
  17. package/stories/components/Letter.stories.js +30 -0
  18. package/stories/components/PostAuthor.stories.js +32 -0
  19. package/stories/components/QRcode.stories.js +37 -0
  20. package/stories/components/SkillMartial.stories.js +31 -0
  21. package/stories/exports/Article.stories.js +48 -0
  22. package/stories/exports/BoxResource.stories.js +31 -0
  23. package/stories/exports/Buff.stories.js +38 -0
  24. package/stories/exports/GameText.stories.js +38 -0
  25. package/stories/exports/Item.stories.js +51 -0
  26. package/stories/exports/ItemSimple.stories.js +50 -0
  27. package/stories/exports/Markdown.stories.js +46 -0
  28. package/stories/exports/Npc.stories.js +35 -0
  29. package/stories/exports/Resource.stories.js +29 -0
  30. package/stories/exports/Skill.stories.js +38 -0
  31. package/stories/exports/Tinymce.stories.js +46 -0
  32. package/stories/exports/Upload.stories.js +41 -0
  33. package/stories/exports/UploadAlbum.stories.js +31 -0
@@ -1,13 +1,13 @@
1
1
  <template>
2
2
  <!-- @圈人pop:作者卡片 -->
3
3
  <div class="w-author" v-loading="loading">
4
- <div class="w-author-wrapper el-popover" v-if="data" :style="{ backgroundImage: `url(${bg})` }">
4
+ <div class="w-author-wrapper el-popover" :class="{ 'is-no-atcard': !bg }" v-if="data" :style="authorCardStyle">
5
5
  <div class="u-author">
6
6
  <Avatar
7
7
  class="u-avatar"
8
8
  :uid="uid"
9
9
  :url="data.user_avatar"
10
- :size="68"
10
+ :size="60"
11
11
  :frame="data.user_avatar_frame"
12
12
  />
13
13
  <div class="u-info">
@@ -58,29 +58,61 @@
58
58
  </div>
59
59
  </div>
60
60
  </div>
61
- <div class="u-teams" v-if="teams && teams.length">
62
- <a class="u-team" v-for="(item, i) in teams" :key="i" :href="teamLink(item.team_id)" target="_blank">
63
- <img class="u-team-logo" :src="showTeamLogo(item.team_logo)" />
64
- <span class="u-team-name">{{ item.team_name }}@{{ item.team_server }}</span>
65
- </a>
61
+ <div class="u-author-role" v-if="authorRoles.length">
62
+ <div class="u-author-role-label"></div>
63
+ <div class="u-author-roles">
64
+ <component
65
+ :is="item.url ? 'a' : 'span'"
66
+ class="u-author-role-item"
67
+ :href="item.url"
68
+ :target="item.url ? '_blank' : null"
69
+ :style="roleStyle(item)"
70
+ v-for="item in authorRoles"
71
+ :key="item.key"
72
+ >
73
+ <img class="u-author-role-icon" v-if="item.icon" :src="item.icon" />
74
+ <span class="u-author-role-content">
75
+ <span class="u-author-role-name">{{ item.name }}</span>
76
+ <span class="u-author-role-server" v-if="item.server">@{{ item.server }}</span>
77
+ </span>
78
+ </component>
79
+ </div>
66
80
  </div>
67
81
  </div>
68
82
  </div>
69
83
  </template>
70
84
 
71
85
  <script>
72
- import { authorLink, getLink, getMedalLink, getThumbnail } from "@jx3box/jx3box-common/js/utils";
73
- import { getUserInfo, getUserMedals, getUserPublicTeams } from "../service/author.js";
74
- import { getDecoration, getDecorationJson } from "../service/cms.js";
86
+ import { authorLink, getMedalLink, showSchoolIcon } from "@jx3box/jx3box-common/js/utils";
87
+ import { getUserInfo, getUserMedals } from "../service/author.js";
88
+ import { getDecoration, getDecorationJson, getUserSkin } from "../service/cms.js";
75
89
  import User from "@jx3box/jx3box-common/js/user";
76
90
  import JX3BOX from "@jx3box/jx3box-common/data/jx3box.json";
77
91
  import Avatar from "./Avatar.vue";
78
- const ATCARD_KEY = "decoration_atcard";
92
+ const ATCARD_CACHE_KEY = "skin_atcard_pc_authorcard_v2_";
93
+ const ATCARD_SUBTYPE = "pc_authorcard";
79
94
  const DECORATION_JSON = "decoration_json";
80
95
  const DECORATION_KEY = "decoration_me";
81
96
  const HONOR_KEY = "honor_me";
97
+ const POSITION_MAP = {
98
+ lt: "left top",
99
+ mt: "center top",
100
+ ct: "center top",
101
+ rt: "right top",
102
+ lm: "left center",
103
+ ml: "left center",
104
+ mm: "center center",
105
+ cm: "center center",
106
+ o: "center center",
107
+ rm: "right center",
108
+ mr: "right center",
109
+ lb: "left bottom",
110
+ mb: "center bottom",
111
+ cb: "center bottom",
112
+ rb: "right bottom",
113
+ };
82
114
 
83
- const { __server, __imgPath, __userLevelColor, __cdn, __userLevel } = JX3BOX;
115
+ const { __server, __imgPath, __userLevelColor, __cdn, __Root, __userLevel } = JX3BOX;
84
116
  export default {
85
117
  name: "Author",
86
118
  components: {
@@ -91,13 +123,17 @@ export default {
91
123
  type: [String, Number],
92
124
  required: true,
93
125
  },
126
+ visible: {
127
+ type: Boolean,
128
+ default: true,
129
+ },
94
130
  },
95
131
  data: () => ({
96
132
  data: null,
97
133
  medals: [],
98
- teams: [],
99
134
  loading: false,
100
135
  bg: "",
136
+ bgPosition: "right top",
101
137
  honor: "",
102
138
  honorStyle: {},
103
139
  }),
@@ -123,6 +159,18 @@ export default {
123
159
  isSuperAuthor: function () {
124
160
  return !!this.data?.sign;
125
161
  },
162
+ authorRoles: function () {
163
+ const roles = this.normalizeRoles(this.data?.public_roles);
164
+ return roles.map(this.normalizeRole).filter((item) => item.name);
165
+ },
166
+ authorCardStyle: function () {
167
+ return this.bg
168
+ ? {
169
+ backgroundImage: `url(${this.bg})`,
170
+ backgroundPosition: this.bgPosition,
171
+ }
172
+ : {};
173
+ },
126
174
  },
127
175
  watch: {
128
176
  uid: {
@@ -130,20 +178,28 @@ export default {
130
178
  handler(val) {
131
179
  if (val) {
132
180
  this.loadData();
133
- this.getAtcard();
181
+ if (this.visible) {
182
+ this.getAtcard();
183
+ }
134
184
  // this.getHonor();
135
185
  }
136
186
  },
137
187
  },
188
+ visible: {
189
+ handler(val) {
190
+ if (val && this.uid) {
191
+ this.getAtcard();
192
+ }
193
+ },
194
+ },
138
195
  },
139
196
  methods: {
140
197
  loadData: function () {
141
- const promises = [getUserInfo(this.uid), getUserMedals(this.uid), getUserPublicTeams(this.uid)];
198
+ const promises = [getUserInfo(this.uid), getUserMedals(this.uid)];
142
199
  this.loading = true;
143
200
  Promise.all(promises).then((res) => {
144
201
  this.data = res[0];
145
202
  this.medals = res[1];
146
- this.teams = res[2];
147
203
  this.loading = false;
148
204
  });
149
205
  },
@@ -157,36 +213,98 @@ export default {
157
213
  this.medals = data;
158
214
  });
159
215
  },
160
- loadTeams: function () {
161
- return getUserPublicTeams(this.uid).then((data) => {
162
- this.teams = data && data.slice(0, 5);
163
- });
164
- },
165
216
  getAtcard() {
166
- let decoration_atcard = sessionStorage.getItem(ATCARD_KEY + this.uid);
167
- if (decoration_atcard == "no") {
168
- this.bg = "";
217
+ const uid = this.uid;
218
+ this.bg = "";
219
+ this.bgPosition = "right top";
220
+
221
+ const cached = this.getAtcardCache(uid);
222
+ if (cached) {
223
+ this.setAtcardBackground(cached === "no" ? null : cached);
169
224
  return;
170
225
  }
171
- //已有缓存,读取解析
172
- if (decoration_atcard) {
173
- this.setDecoration(decoration_atcard);
226
+
227
+ getUserSkin({
228
+ user_id: uid,
229
+ type: "atcard",
230
+ })
231
+ .then((data) => {
232
+ if (this.uid !== uid) return;
233
+
234
+ let rows = data?.data?.data || [];
235
+ let skin = this.selectAtcardSkin(this.flattenUserSkins(rows));
236
+ let image = this.showDecorationImage(skin?.image);
237
+ if (!image) {
238
+ this.setAtcardCache(uid, "no");
239
+ this.bg = "";
240
+ return;
241
+ }
242
+ let payload = {
243
+ image,
244
+ position: this.resolveAtcardPosition(skin?.position),
245
+ };
246
+ this.setAtcardCache(uid, payload);
247
+ this.setAtcardBackground(payload);
248
+ })
249
+ .catch(() => {
250
+ if (this.uid === uid) {
251
+ this.bg = "";
252
+ }
253
+ });
254
+ },
255
+ setAtcardBackground(val) {
256
+ if (typeof val === "string") {
257
+ this.bg = val || "";
258
+ this.bgPosition = "right top";
174
259
  return;
175
260
  }
176
- getDecoration({ using: 1, user_id: this.uid, type: "atcard" }).then((data) => {
177
- let res = data.data.data;
178
- if (res.length == 0) {
179
- //空 则为无主题,不再加载接口,界面设No
180
- sessionStorage.setItem(ATCARD_KEY + this.uid, "no");
181
- this.bg = "";
182
- return;
183
- }
184
- sessionStorage.setItem(ATCARD_KEY + this.uid, res[0].val);
185
- this.setDecoration(res[0].val);
186
- });
261
+ this.bg = val?.image || "";
262
+ this.bgPosition = val?.position || "right top";
263
+ },
264
+ getAtcardCache(uid) {
265
+ let cached = sessionStorage.getItem(ATCARD_CACHE_KEY + uid);
266
+ if (!cached) return null;
267
+ if (cached === "no") return cached;
268
+ try {
269
+ return JSON.parse(cached);
270
+ } catch (e) {
271
+ sessionStorage.removeItem(ATCARD_CACHE_KEY + uid);
272
+ return null;
273
+ }
274
+ },
275
+ setAtcardCache(uid, val) {
276
+ if (!uid) return;
277
+ sessionStorage.setItem(ATCARD_CACHE_KEY + uid, val === "no" ? "no" : JSON.stringify(val));
278
+ },
279
+ flattenUserSkins(rows = []) {
280
+ return rows.reduce((list, row) => {
281
+ let skins = Array.isArray(row?.skins) ? row.skins : [];
282
+ return list.concat(skins);
283
+ }, []);
284
+ },
285
+ selectAtcardSkin(list = []) {
286
+ let skins = list.filter((item) => item?.subtype === ATCARD_SUBTYPE && item?.image);
287
+ let theme = this.getCurrentTheme();
288
+ return (
289
+ skins.find((item) => item?.theme === "all") ||
290
+ skins.find((item) => item?.theme === theme) ||
291
+ skins.find((item) => !item?.theme) ||
292
+ null
293
+ );
294
+ },
295
+ getCurrentTheme() {
296
+ let theme =
297
+ document.documentElement.getAttribute("data-theme") ||
298
+ document.body?.getAttribute("data-theme") ||
299
+ localStorage.getItem("__theme") ||
300
+ "light";
301
+ return String(theme || "light").toLowerCase() === "dark" ? "dark" : "light";
187
302
  },
188
- setDecoration(val) {
189
- this.bg = this.showDecoration(val, "atcard");
303
+ resolveAtcardPosition(position) {
304
+ let key = String(position || "rt")
305
+ .trim()
306
+ .toLowerCase();
307
+ return POSITION_MAP[key] || POSITION_MAP.rt;
190
308
  },
191
309
  getHonor() {
192
310
  this.honor = "";
@@ -265,7 +383,7 @@ export default {
265
383
  },
266
384
 
267
385
  showMedalIcon: function (val) {
268
- return __cdn + "/design/medals/user/" + val + ".gif";
386
+ return __cdn + "/design/medals/user/" + val + ".webp";
269
387
  },
270
388
  medalLink: function ({ rank_id, medal_type = "rank" }) {
271
389
  return getMedalLink(rank_id, medal_type);
@@ -273,17 +391,87 @@ export default {
273
391
  showMedalDesc: function (item) {
274
392
  return item.medal_desc || medal_map[item.medal];
275
393
  },
276
- teamLink: function (team_id) {
277
- return getLink("org", team_id);
278
- },
279
- showTeamLogo: function (val) {
280
- return getThumbnail(val, 96);
281
- },
282
394
  showLevelColor: function (level) {
283
395
  return __userLevelColor[level];
284
396
  },
285
- showDecoration: function (val, type) {
286
- return __cdn + `design/decoration/images/${val}/${type}.png`;
397
+ showDecorationImage: function (val) {
398
+ if (!val) return "";
399
+ if (/^(https?:)?\/\//.test(val) || /^(data|blob):/.test(val)) return val;
400
+ return __cdn.replace(/\/$/, "") + "/" + val.replace(/^\//, "");
401
+ },
402
+ normalizeRoles: function (roles) {
403
+ if (!roles) return [];
404
+ if (Array.isArray(roles)) return roles;
405
+ if (typeof roles === "string") {
406
+ try {
407
+ const parsed = JSON.parse(roles);
408
+ return Array.isArray(parsed) ? parsed : [parsed];
409
+ } catch (e) {
410
+ return roles
411
+ .split(/[,,]/)
412
+ .map((item) => item.trim())
413
+ .filter(Boolean);
414
+ }
415
+ }
416
+ return [roles];
417
+ },
418
+ normalizeRole: function (role, index) {
419
+ if (typeof role === "string") {
420
+ return {
421
+ key: role + index,
422
+ name: role,
423
+ };
424
+ }
425
+
426
+ const info = role?.role_info || role?.info || {};
427
+ const name =
428
+ role?.name ||
429
+ role?.title ||
430
+ role?.label ||
431
+ role?.role_name ||
432
+ role?.display_name ||
433
+ role?.remark ||
434
+ info?.name ||
435
+ info?.title ||
436
+ info?.label ||
437
+ info?.role_name ||
438
+ "";
439
+
440
+ return {
441
+ key: role?.id || role?.key || role?.val || name + index,
442
+ name,
443
+ server: role?.server || role?.role_server || role?.jx3_server || info?.server || info?.role_server || "",
444
+ url: this.normalizeLink(role?.url || role?.link || role?.href || info?.url || info?.link || ""),
445
+ icon: this.normalizeRoleIcon(role, info),
446
+ color: role?.color || info?.color || "",
447
+ backgroundColor: role?.background_color || role?.bg_color || info?.background_color || info?.bg_color || "",
448
+ borderColor: role?.border_color || info?.border_color || "",
449
+ };
450
+ },
451
+ normalizeRoleAsset: function (url) {
452
+ if (!url) return "";
453
+ url = String(url).trim();
454
+ if (/^(https?:)?\/\//.test(url)) return url;
455
+ return __cdn + url.replace(/^\/+/, "");
456
+ },
457
+ normalizeRoleIcon: function (role, info) {
458
+ const mount = role?.mount || role?.school || role?.school_id || info?.mount || info?.school || info?.school_id || "";
459
+ if (mount) return showSchoolIcon(mount);
460
+
461
+ return this.normalizeRoleAsset(role?.icon || role?.logo || role?.image || info?.icon || info?.logo || "");
462
+ },
463
+ normalizeLink: function (url) {
464
+ if (!url) return "";
465
+ url = String(url).trim();
466
+ if (/^(https?:)?\/\//.test(url)) return url;
467
+ return __Root + url.replace(/^\/+/, "");
468
+ },
469
+ roleStyle: function (role) {
470
+ return {
471
+ color: role.color || null,
472
+ backgroundColor: role.backgroundColor || null,
473
+ borderColor: role.borderColor || null,
474
+ };
287
475
  },
288
476
  authorLink,
289
477
  },
@@ -294,15 +482,31 @@ export default {
294
482
  @import "../assets/css/module/author.less";
295
483
  .w-author {
296
484
  .w-author-wrapper {
485
+ width: 300px;
486
+ min-width: 300px;
487
+ max-width: 300px;
488
+ padding: 12px;
489
+ border: 1px solid rgba(132, 146, 166, 0.28);
490
+ border-radius: 8px;
491
+ background-color: #fff;
297
492
  background-repeat: no-repeat;
298
493
  background-position: top right;
299
494
  background-size: 100% auto;
495
+ box-shadow: 0 14px 34px rgba(31, 41, 55, 0.16), 0 2px 8px rgba(31, 41, 55, 0.08);
496
+ &.is-no-atcard {
497
+ border-color: rgba(124, 134, 156, 0.34);
498
+ background: linear-gradient(180deg, #ffffff 0%, #f3f6fb 100%);
499
+ }
300
500
  .u-author {
301
- padding: 5px 0 15px 5px;
501
+ padding: 4px 10px 12px 5px;
302
502
  }
303
503
  .u-avatar {
304
504
  .fl;
305
- .mr(15px);
505
+ .mr(12px);
506
+ .size(60px);
507
+ }
508
+ .u-info {
509
+ height: 60px;
306
510
  }
307
511
  img {
308
512
  border: none;
@@ -320,7 +524,7 @@ export default {
320
524
  .u-displayname {
321
525
  .fz(15px);
322
526
  .nobreak;
323
- max-width: 200px;
527
+ max-width: 190px;
324
528
  color: @color;
325
529
  .bold;
326
530
  }
@@ -336,5 +540,61 @@ export default {
336
540
  .mb(15px);
337
541
  .r(2px);
338
542
  }
543
+ .u-author-role {
544
+ .mt(8px);
545
+ }
546
+ .u-author-role-label {
547
+ margin: 0 0 8px 0;
548
+ height: 1px;
549
+ background-color: rgba(154, 167, 181, 0.28);
550
+ }
551
+ .u-author-roles {
552
+ display: flex;
553
+ flex-direction: column;
554
+ gap: 4px;
555
+ }
556
+ .u-author-role-item {
557
+ .flex(y);
558
+ gap: 7px;
559
+ max-width: 100%;
560
+ min-height: 21px;
561
+ color: #56677a;
562
+ font-size: 12px;
563
+ line-height: 21px;
564
+ box-sizing: border-box;
565
+ text-decoration: none;
566
+
567
+ &:hover {
568
+ .u-author-role-server {
569
+ color: #fba524;
570
+ }
571
+ }
572
+ }
573
+ .u-author-role-icon {
574
+ .size(20px);
575
+ flex-shrink: 0;
576
+ object-fit: contain;
577
+ }
578
+ .u-author-role-content {
579
+ min-width: 0;
580
+ display: flex;
581
+ align-items: baseline;
582
+ }
583
+ .u-author-role-name {
584
+ min-width: 0;
585
+ margin-right: 2px;
586
+ overflow: hidden;
587
+ text-overflow: ellipsis;
588
+ white-space: nowrap;
589
+ color: #42566d;
590
+ font-size: 12px;
591
+ font-weight: 400;
592
+ }
593
+ .u-author-role-server {
594
+ flex-shrink: 0;
595
+ color: #fba524;
596
+ font-size: 12px;
597
+ font-weight: 400;
598
+ }
339
599
  }
340
600
  </style>
@@ -11,7 +11,7 @@
11
11
  <div class="u-qrcode">
12
12
  <qrcode-vue class="u-pic" :value="value" :size="size" level="H"></qrcode-vue>
13
13
  <span class="u-txt"
14
- ><img class="u-icon" svg-inline src="./assets/img/other/qr-code.svg" />扫一扫手机访问</span
14
+ ><img class="u-icon" svg-inline src="../assets/img/other/qr-code.svg" />扫一扫手机访问</span
15
15
  >
16
16
  </div>
17
17
  </div>
@@ -28,6 +28,18 @@ function getDecoration(params) {
28
28
  });
29
29
  }
30
30
 
31
+ function getDecorationV2(params) {
32
+ return $cms().get(`/api/cms/user/decoration/v2`, {
33
+ params,
34
+ });
35
+ }
36
+
37
+ function getUserSkin(params) {
38
+ return $cms().get(`/api/cms/user/skin`, {
39
+ params,
40
+ });
41
+ }
42
+
31
43
  function getDecorationJson() {
32
44
  let url = __cdn + "design/decoration/index.json";
33
45
  return axios.get(url);
@@ -39,4 +51,4 @@ function getLetterPaper(params) {
39
51
  params,
40
52
  });
41
53
  }
42
- export { uploadFile, loadAuthors, loadEmotions, getDecoration, getDecorationJson, getLetterPaper };
54
+ export { uploadFile, loadAuthors, loadEmotions, getDecoration, getDecorationV2, getUserSkin, getDecorationJson, getLetterPaper };
@@ -0,0 +1 @@
1
+ @bg-black: #111827;
@@ -0,0 +1,154 @@
1
+ import { onMounted, ref } from "vue";
2
+
3
+ export function createMeta({ title, component, args = {}, argTypes = {}, docs = "", parameters = {} }) {
4
+ return {
5
+ title,
6
+ component,
7
+ tags: ["autodocs"],
8
+ args,
9
+ argTypes: buildArgTypes(argTypes),
10
+ parameters: {
11
+ layout: "padded",
12
+ docs: {
13
+ description: {
14
+ component: docs,
15
+ },
16
+ },
17
+ ...parameters,
18
+ },
19
+ };
20
+ }
21
+
22
+ export function componentStory(component, options = {}) {
23
+ const { template, style = "", components = {}, setup } = options;
24
+
25
+ return {
26
+ render: (args) => ({
27
+ components: {
28
+ StoryComponent: component,
29
+ ...components,
30
+ },
31
+ setup() {
32
+ return {
33
+ args,
34
+ ...(typeof setup === "function" ? setup(args) : {}),
35
+ };
36
+ },
37
+ template:
38
+ template ||
39
+ `<div style="${style}"><StoryComponent v-bind="args" /></div>`,
40
+ }),
41
+ };
42
+ }
43
+
44
+ export function previewStory(component, options = {}) {
45
+ const { title = "效果预览", description = "", code = "", style = "", template, components = {}, setup } = options;
46
+
47
+ return {
48
+ render: (args) => ({
49
+ components: {
50
+ StoryComponent: component,
51
+ ...components,
52
+ },
53
+ setup() {
54
+ return {
55
+ args,
56
+ previewTitle: title,
57
+ previewDescription: description,
58
+ previewCode: code,
59
+ ...(typeof setup === "function" ? setup(args) : {}),
60
+ };
61
+ },
62
+ template: `
63
+ <div style="display:grid;gap:16px;min-width:320px;">
64
+ <div style="padding:16px 18px;border:1px solid #e5e7eb;border-radius:12px;background:#fff;">
65
+ <div style="font-size:16px;font-weight:600;color:#111827;">{{ previewTitle }}</div>
66
+ <div v-if="previewDescription" style="margin-top:8px;color:#4b5563;line-height:1.7;white-space:pre-line;">{{ previewDescription }}</div>
67
+ <pre v-if="previewCode" style="margin:12px 0 0;padding:12px;border-radius:10px;background:#0f172a;color:#e2e8f0;font-size:12px;line-height:1.6;overflow:auto;white-space:pre-wrap;word-break:break-word;">{{ previewCode }}</pre>
68
+ </div>
69
+ <div style="padding:20px;border:1px solid #e5e7eb;border-radius:12px;background:#fff;${style}">
70
+ ${template || `<StoryComponent v-bind="args" />`}
71
+ </div>
72
+ </div>
73
+ `,
74
+ }),
75
+ };
76
+ }
77
+
78
+ export function docsOnlyStory(message) {
79
+ return {
80
+ render: () => ({
81
+ template: `<div style="max-width:720px;padding:16px 18px;border:1px solid #e5e7eb;border-radius:12px;background:#fff;color:#374151;line-height:1.7;">${escapeHtml(
82
+ message
83
+ )}</div>`,
84
+ }),
85
+ };
86
+ }
87
+
88
+ export function useDemoText(url, fallback = "") {
89
+ const content = ref(fallback);
90
+ const loaded = ref(false);
91
+
92
+ onMounted(async () => {
93
+ try {
94
+ const response = await fetch(url);
95
+ if (response.ok) {
96
+ content.value = await response.text();
97
+ }
98
+ } catch (error) {
99
+ console.warn(`[storybook] failed to load demo content: ${url}`, error);
100
+ } finally {
101
+ loaded.value = true;
102
+ }
103
+ });
104
+
105
+ return {
106
+ content,
107
+ loaded,
108
+ };
109
+ }
110
+
111
+ function buildArgTypes(argTypes) {
112
+ return Object.fromEntries(
113
+ Object.entries(argTypes).map(([name, config]) => {
114
+ const table = {};
115
+
116
+ if (config.type) {
117
+ table.type = { summary: config.type };
118
+ }
119
+
120
+ if (config.defaultValue !== undefined) {
121
+ table.defaultValue = { summary: config.defaultValue };
122
+ }
123
+
124
+ if (config.required) {
125
+ table.category = "required";
126
+ }
127
+
128
+ return [
129
+ name,
130
+ {
131
+ description: config.description,
132
+ control: config.control === false ? false : config.control || inferControl(config.type),
133
+ options: config.options,
134
+ table,
135
+ },
136
+ ];
137
+ })
138
+ );
139
+ }
140
+
141
+ function inferControl(type = "") {
142
+ if (type.includes("Boolean")) return "boolean";
143
+ if (type.includes("Number")) return "number";
144
+ if (type.includes("Array") || type.includes("Object")) return "object";
145
+ return "text";
146
+ }
147
+
148
+ function escapeHtml(value) {
149
+ return String(value)
150
+ .replace(/&/g, "&amp;")
151
+ .replace(/</g, "&lt;")
152
+ .replace(/>/g, "&gt;")
153
+ .replace(/\n/g, "<br />");
154
+ }