@wu529778790/floating-ad 0.1.3 → 0.1.4

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
@@ -8,10 +8,10 @@
8
8
 
9
9
  - 🧩 Web Component 封装,一行 `<script>` 注入、改一处全站生效
10
10
  - 🎯 声明式标签 `<floating-ad>`,属性改动即时热更新
11
- - 🖼 图片固定 120×120 方形(与右侧公众号浮窗的二维码同尺寸),`cover` / `contain` 可选,多张混排自动分隔线
11
+ - 🖼 图片正方形、铺满卡片宽度(卡片无内边距,视觉更大),`cover` / `contain` 可选,多张混排自动分隔线
12
12
  - 🏷 可选的「广告」标识(默认不显示),开启后压首图左上角、不占排版行高
13
13
  - 🎨 CSS 变量主题化(`--fa-*`),一行覆盖背景色 / 强调色 / 圆角 / 边框
14
- - 📱 移动端小一号展示(卡片 140 / 图片 112),默认落在左下角,可配置隐藏
14
+ - 📱 移动端小一号展示(卡片 140,图片同样铺满),两端都默认落在左侧垂直居中,可配置隐藏
15
15
  - 🧹 关闭即消失,默认不记住状态(刷新必重新出现)
16
16
  - 🚫 没有广告数据时不渲染任何节点,不会留一张空白卡片
17
17
  - 📦 NPM + CDN 双通道引入,TypeScript 类型齐全
@@ -86,7 +86,7 @@ new FloatingAd({
86
86
 
87
87
  | 属性 | 类型 | 默认值 | 说明 |
88
88
  | --- | --- | --- | --- |
89
- | `position` | `string` | 桌面 `left-center`<br>移动 `left-bottom` | `left-center` / `left-bottom` / `left-top` / `right-center` / `right-bottom` / `right-top` |
89
+ | `position` | `string` | `left-center`(桌面/移动一致) | `left-center` / `left-bottom` / `left-top` / `right-center` / `right-bottom` / `right-top` |
90
90
  | `width` | `number` | 桌面 150<br>移动 140 | 浮窗宽度(px) |
91
91
  | `offset` | `number` | 桌面 16<br>移动 12 | 距视口边缘距离(px) |
92
92
  | `close-persistence` | `boolean` | 关 | 打开时关闭写入 localStorage,刷新不再出现 |
@@ -94,7 +94,7 @@ new FloatingAd({
94
94
  | `z-index` | `number` | `9999` | 浮窗层级 |
95
95
  | `ad-label` | `string` | 不显示 | 标识文案(压在首图左上角,不占行高);空值 / `true` 显示「广告」,`false` / `none` / `off` / `0` 关闭 |
96
96
  | `image-fit` | `string` | `'cover'` | 图片填充方式:`cover` / `contain` |
97
- | `image-size` | `number` | 桌面 120<br>移动 112 | 广告图片边长(px,正方形) |
97
+ | `image-size` | `number` | 铺满卡片宽度 | 广告图片边长上限(px,正方形);小于卡片宽度时居中 |
98
98
  | `ad-src` | `string` | - | 单条广告图片 URL(必填,缺省不渲染) |
99
99
  | `ad-title` | `string` | - | 单条广告标题 |
100
100
  | `ad-desc` | `string` | - | 单条广告副文案 |
@@ -118,7 +118,7 @@ new FloatingAd({
118
118
  | 字段 | 类型 | 默认值 | 说明 |
119
119
  | --- | --- | --- | --- |
120
120
  | `ads` | `FloatingAdItem[]` | `[]` | 广告列表,见下 |
121
- | `position` | `Position` | 桌面 `left-center`<br>移动 `left-bottom` | 位置 |
121
+ | `position` | `Position` | `'left-center'`(桌面/移动一致) | 位置 |
122
122
  | `closePersistence` | `boolean` | `false` | `true` 时关闭写入 localStorage,刷新不再出现 |
123
123
  | `hideOnMobile` | `boolean` | `true` | `<768px` 时不渲染 |
124
124
  | `zIndex` | `number` | `9999` | 浮窗层级 |
@@ -126,7 +126,7 @@ new FloatingAd({
126
126
  | `width` | `number` | 桌面 150<br>移动 140 | 宽度(px) |
127
127
  | `offset` | `number` | 桌面 16<br>移动 12 | 距边缘(px) |
128
128
  | `imageFit` | `'cover' \| 'contain'` | `'cover'` | 图片填充 |
129
- | `imageSize` | `number` | 桌面 120<br>移动 112 | 广告图片边长(px,正方形) |
129
+ | `imageSize` | `number` | 铺满卡片宽度 | 广告图片边长上限(px,正方形);小于卡片宽度时居中 |
130
130
  | `theme.bg` | `string` | 半透明白 | 背景色 |
131
131
  | `theme.accent` | `string` | `#333` | 强调色 |
132
132
  | `theme.radius` | `string` | `12px` | 圆角 |
@@ -174,12 +174,13 @@ Web Component 元素同样提供 `open()` / `close()`。
174
174
 
175
175
  | 项 | 桌面(≥768px) | 移动端(<768px) |
176
176
  | --- | --- | --- |
177
- | 位置 | `left-center`(左侧垂直居中) | `left-bottom`(左下角) |
177
+ | 位置 | `left-center`(左侧垂直居中) | `left-center`(同样左侧居中) |
178
178
  | 卡片宽度 | 150px | 140px |
179
- | 图片边长 | 120px | 112px |
179
+ | 图片边长 | 铺满卡片宽度(148px) | 铺满卡片宽度(138px) |
180
180
  | 距视口边缘 | 16px | 12px |
181
181
  | 关闭按钮 | 悬停出现 | 常显(触屏无 hover) |
182
182
 
183
+ - 卡片无内边距,图片直接铺满;想缩小图片传 `image-size`(小于卡片宽度时居中)
183
184
  - 显式设置 `position` / `width` / `offset` / `imageSize` 时会覆盖两端默认值(移动端也用同一尺寸)
184
185
  - 想彻底不在移动端出现:`hide-on-mobile`
185
186
  - 想改移动端尺寸又保留响应式:直接覆盖 CSS 变量(不要传 `width` 等 JS 参数)
@@ -188,7 +189,7 @@ Web Component 元素同样提供 `open()` / `close()`。
188
189
  @media (max-width: 767px) {
189
190
  .fa-widget {
190
191
  --fa-width: 130px;
191
- --fa-img-size: 104px;
192
+ --fa-img-size: 104px; /* 留空/100% 则铺满卡片 */
192
193
  --fa-offset: 10px;
193
194
  }
194
195
  }
@@ -207,7 +208,7 @@ Web Component 元素同样提供 `open()` / `close()`。
207
208
  --fa-offset: 24px; /* 距视口边缘距离 */
208
209
  --fa-width: 180px; /* 浮窗宽度 */
209
210
  --fa-img-fit: cover; /* 图片填充方式 */
210
- --fa-img-size: 120px; /* 广告图片边长(正方形) */
211
+ --fa-img-size: 120px; /* 图片边长上限(默认 100% 铺满卡片) */
211
212
  }
212
213
  ```
213
214
 
@@ -5,14 +5,17 @@
5
5
  --fa-border: rgba(0, 0, 0, 0.1);
6
6
  --fa-offset: 16px;
7
7
  --fa-width: 150px;
8
- --fa-img-size: 120px;
8
+ /* 图片边长上限:默认 100% 让图片铺满卡片(无内边距),
9
+ 显式传 imageSize / --fa-img-size 时才小于卡片宽度并居中 */
10
+ --fa-img-size: 100%;
9
11
  --fa-img-fit: cover;
10
12
 
11
13
  position: fixed;
12
14
  z-index: 9999;
13
15
  box-sizing: border-box;
14
16
  width: var(--fa-width);
15
- padding: 14px;
17
+ padding: 0;
18
+ overflow: hidden;
16
19
  background: var(--fa-bg);
17
20
  border: 1px solid var(--fa-border);
18
21
  border-radius: var(--fa-radius);
@@ -64,8 +67,9 @@
64
67
 
65
68
  .fa-close {
66
69
  position: absolute;
67
- top: -2px;
68
- right: -2px;
70
+ top: 6px;
71
+ right: 6px;
72
+ z-index: 2;
69
73
  display: flex;
70
74
  align-items: center;
71
75
  justify-content: center;
@@ -75,14 +79,18 @@
75
79
  margin: 0;
76
80
  border: none;
77
81
  border-radius: 50%;
78
- background: transparent;
79
- color: rgba(0, 0, 0, 0.35);
82
+ /* 图片铺满卡片后按钮压在图上,用半透明底保证任何图都能看清 */
83
+ background: rgba(0, 0, 0, 0.38);
84
+ color: rgba(255, 255, 255, 0.95);
85
+ backdrop-filter: blur(4px);
86
+ -webkit-backdrop-filter: blur(4px);
80
87
  cursor: pointer;
81
- transition: color 0.15s ease, opacity 0.15s ease;
88
+ transition: background 0.15s ease, opacity 0.15s ease;
82
89
  }
83
90
 
84
91
  .fa-close:hover {
85
- color: rgba(0, 0, 0, 0.7);
92
+ background: rgba(0, 0, 0, 0.58);
93
+ color: #fff;
86
94
  }
87
95
 
88
96
  /* 触屏设备(无 hover):关闭按钮常显,保证可关闭 */
@@ -134,15 +142,17 @@ a.fa-item {
134
142
  cursor: pointer;
135
143
  }
136
144
 
137
- /* 图片固定方形(默认 120×120,与右侧公众号浮窗的二维码尺寸一致) */
145
+ /* 图片正方形,默认铺满卡片宽度(卡片无内边距);
146
+ 显式传 imageSize 时作为边长上限,图片在卡片内居中 */
138
147
  .fa-media {
139
148
  position: relative;
140
149
  display: block;
141
- width: var(--fa-img-size, 120px);
142
- height: var(--fa-img-size, 120px);
150
+ width: 100%;
151
+ max-width: var(--fa-img-size, 100%);
152
+ aspect-ratio: 1 / 1;
143
153
  margin: 0 auto;
144
154
  overflow: hidden;
145
- border-radius: 8px;
155
+ border-radius: 0;
146
156
  background: rgba(0, 0, 0, 0.04);
147
157
  }
148
158
 
@@ -167,7 +177,8 @@ a.fa-item:hover .fa-img {
167
177
  .fa-tag {
168
178
  position: absolute;
169
179
  top: 6px;
170
- right: 6px;
180
+ /* 让开关闭按钮(右上角 6px + 22px) */
181
+ right: 34px;
171
182
  padding: 1px 6px;
172
183
  font-size: 10px;
173
184
  line-height: 1.5;
@@ -176,14 +187,15 @@ a.fa-item:hover .fa-img {
176
187
  color: var(--fa-bg);
177
188
  }
178
189
 
190
+ /* 卡片已无内边距,文字区自带左右留白 */
179
191
  .fa-body {
180
192
  display: block;
181
- margin-top: 8px;
193
+ padding: 8px 12px 12px;
182
194
  text-align: left;
183
195
  }
184
196
 
185
197
  .fa-item > .fa-body:first-child {
186
- margin-top: 0;
198
+ padding-top: 12px;
187
199
  }
188
200
 
189
201
  .fa-title {
@@ -202,17 +214,15 @@ a.fa-item:hover .fa-img {
202
214
 
203
215
  .fa-divider {
204
216
  height: 1px;
205
- margin: 10px 0;
217
+ margin: 8px 12px;
206
218
  background: var(--fa-border);
207
219
  }
208
220
 
209
- /* 移动端:比桌面略小一号(卡片 140 / 图片 112 / 距边 12),缺省落在左下角 */
221
+ /* 移动端:比桌面略小一号(卡片 140 / 图片铺满 / 距边 12),缺省同样落在左侧垂直居中 */
210
222
  @media (max-width: 767px) {
211
223
  .fa-widget {
212
224
  --fa-width: 140px;
213
- --fa-img-size: 112px;
214
225
  --fa-offset: 12px;
215
- padding: 13px;
216
226
  }
217
227
 
218
228
  /* 触屏点按区域稍放大 */
@@ -240,14 +250,6 @@ a.fa-item:hover .fa-img {
240
250
  background: rgba(255, 255, 255, 0.06);
241
251
  }
242
252
 
243
- .fa-close {
244
- color: rgba(255, 255, 255, 0.4);
245
- }
246
-
247
- .fa-close:hover {
248
- color: rgba(255, 255, 255, 0.8);
249
- }
250
-
251
253
  .fa-desc {
252
254
  color: rgba(255, 255, 255, 0.55);
253
255
  }
@@ -1,4 +1,4 @@
1
- /* @wu529778790/floating-ad v0.1.3 */
1
+ /* @wu529778790/floating-ad v0.1.4 */
2
2
  "use strict";
3
3
  (() => {
4
4
  // src/FloatingAd.ts
@@ -39,8 +39,8 @@
39
39
  const raw = Array.isArray(options.ads) ? options.ads : [];
40
40
  return {
41
41
  ads: raw.filter(isRenderableAd),
42
- // 移动端缺省落在左下角(不挡右侧阅读区/返回顶部按钮)
43
- position: (_a = options.position) != null ? _a : isMobile() ? "left-bottom" : "left-center",
42
+ // 桌面与移动端缺省一致:左侧垂直居中(不挡正文,也不压右下角的返回顶部按钮)
43
+ position: (_a = options.position) != null ? _a : "left-center",
44
44
  closePersistence: (_b = options.closePersistence) != null ? _b : false,
45
45
  hideOnMobile: (_c = options.hideOnMobile) != null ? _c : false,
46
46
  zIndex: (_d = options.zIndex) != null ? _d : 9999,
@@ -203,7 +203,7 @@
203
203
  }
204
204
 
205
205
  // src/styles.css
206
- var styles_default = '.fa-widget {\n --fa-bg: rgba(255, 255, 255, 0.96);\n --fa-accent: #333;\n --fa-radius: 12px;\n --fa-border: rgba(0, 0, 0, 0.1);\n --fa-offset: 16px;\n --fa-width: 150px;\n --fa-img-size: 120px;\n --fa-img-fit: cover;\n\n position: fixed;\n z-index: 9999;\n box-sizing: border-box;\n width: var(--fa-width);\n padding: 14px;\n background: var(--fa-bg);\n border: 1px solid var(--fa-border);\n border-radius: var(--fa-radius);\n box-shadow: 0 2px 10px rgba(0, 0, 0, 0.04);\n font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,\n "Helvetica Neue", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei",\n sans-serif;\n color: var(--fa-accent);\n line-height: 1.5;\n color-scheme: light dark;\n transition: box-shadow 0.2s ease;\n}\n\n.fa-widget:hover {\n box-shadow: 0 6px 20px rgba(0, 0, 0, 0.08);\n}\n\n.fa-widget[data-position="right-bottom"] {\n right: var(--fa-offset);\n bottom: var(--fa-offset);\n}\n\n.fa-widget[data-position="right-top"] {\n right: var(--fa-offset);\n top: var(--fa-offset);\n}\n\n.fa-widget[data-position="left-bottom"] {\n left: var(--fa-offset);\n bottom: var(--fa-offset);\n}\n\n.fa-widget[data-position="left-top"] {\n left: var(--fa-offset);\n top: var(--fa-offset);\n}\n\n.fa-widget[data-position="right-center"] {\n right: var(--fa-offset);\n top: 50%;\n transform: translateY(-50%);\n}\n\n.fa-widget[data-position="left-center"] {\n left: var(--fa-offset);\n top: 50%;\n transform: translateY(-50%);\n}\n\n.fa-close {\n position: absolute;\n top: -2px;\n right: -2px;\n display: flex;\n align-items: center;\n justify-content: center;\n width: 22px;\n height: 22px;\n padding: 0;\n margin: 0;\n border: none;\n border-radius: 50%;\n background: transparent;\n color: rgba(0, 0, 0, 0.35);\n cursor: pointer;\n transition: color 0.15s ease, opacity 0.15s ease;\n}\n\n.fa-close:hover {\n color: rgba(0, 0, 0, 0.7);\n}\n\n/* \u89E6\u5C4F\u8BBE\u5907\uFF08\u65E0 hover\uFF09\uFF1A\u5173\u95ED\u6309\u94AE\u5E38\u663E\uFF0C\u4FDD\u8BC1\u53EF\u5173\u95ED */\n.fa-close {\n opacity: 1;\n}\n\n/* \u652F\u6301 hover \u7684\u8BBE\u5907\uFF08\u9F20\u6807\uFF09\uFF1A\u5E73\u65F6\u9690\u85CF\uFF0C\u60AC\u505C\u5361\u7247\u65F6\u6D6E\u73B0 */\n@media (hover: hover) and (pointer: fine) {\n .fa-close {\n opacity: 0;\n pointer-events: none;\n }\n .fa-widget:hover .fa-close,\n .fa-close:focus-visible {\n opacity: 1;\n pointer-events: auto;\n }\n}\n\n/* \u300C\u5E7F\u544A\u300D\u6807\u8BC6\uFF1A\u538B\u5728\u9996\u56FE\u5DE6\u4E0A\u89D2\uFF0C\u4E0D\u5360\u6392\u7248\u884C\u9AD8\n \uFF08\u300A\u5E7F\u544A\u6CD5\u300B\u8981\u6C42\u5E7F\u544A\u53EF\u8BC6\u522B\uFF0C\u6807\u8BC6\u4FDD\u7559\uFF0C\u4F46\u4E0D\u5FC5\u5355\u72EC\u5360\u4E00\u884C\uFF09 */\n.fa-label {\n position: absolute;\n top: 6px;\n left: 6px;\n z-index: 1;\n display: inline-block;\n font-size: 10px;\n line-height: 1;\n letter-spacing: 0.06em;\n padding: 3px 5px;\n border-radius: 4px;\n background: rgba(0, 0, 0, 0.45);\n color: rgba(255, 255, 255, 0.92);\n backdrop-filter: blur(4px);\n -webkit-backdrop-filter: blur(4px);\n pointer-events: none;\n}\n\n.fa-item {\n display: block;\n text-decoration: none;\n color: inherit;\n border-radius: 8px;\n}\n\na.fa-item {\n cursor: pointer;\n}\n\n/* \u56FE\u7247\u56FA\u5B9A\u65B9\u5F62\uFF08\u9ED8\u8BA4 120\xD7120\uFF0C\u4E0E\u53F3\u4FA7\u516C\u4F17\u53F7\u6D6E\u7A97\u7684\u4E8C\u7EF4\u7801\u5C3A\u5BF8\u4E00\u81F4\uFF09 */\n.fa-media {\n position: relative;\n display: block;\n width: var(--fa-img-size, 120px);\n height: var(--fa-img-size, 120px);\n margin: 0 auto;\n overflow: hidden;\n border-radius: 8px;\n background: rgba(0, 0, 0, 0.04);\n}\n\n.fa-img {\n display: block;\n width: 100%;\n height: 100%;\n object-fit: var(--fa-img-fit, cover);\n transition: transform 0.25s ease;\n}\n\na.fa-item:hover .fa-img {\n transform: scale(1.04);\n}\n\n/* \u56FE\u7247\u52A0\u8F7D\u5931\u8D25\uFF1A\u9690\u85CF\u7A7A\u56FE\u6846\uFF0C\u53EA\u7559\u6587\u5B57\uFF1B\u8FDE\u6587\u5B57\u90FD\u6CA1\u6709\u5C31\u6574\u6761\u9690\u85CF */\n.fa-media--failed,\n.fa-item--failed {\n display: none;\n}\n\n.fa-tag {\n position: absolute;\n top: 6px;\n right: 6px;\n padding: 1px 6px;\n font-size: 10px;\n line-height: 1.5;\n border-radius: 999px;\n background: var(--fa-accent);\n color: var(--fa-bg);\n}\n\n.fa-body {\n display: block;\n margin-top: 8px;\n text-align: left;\n}\n\n.fa-item > .fa-body:first-child {\n margin-top: 0;\n}\n\n.fa-title {\n display: block;\n font-size: 12px;\n font-weight: 500;\n color: var(--fa-accent);\n}\n\n.fa-desc {\n display: block;\n margin-top: 2px;\n font-size: 11px;\n color: rgba(0, 0, 0, 0.45);\n}\n\n.fa-divider {\n height: 1px;\n margin: 10px 0;\n background: var(--fa-border);\n}\n\n/* \u79FB\u52A8\u7AEF\uFF1A\u6BD4\u684C\u9762\u7565\u5C0F\u4E00\u53F7\uFF08\u5361\u7247 140 / \u56FE\u7247 112 / \u8DDD\u8FB9 12\uFF09\uFF0C\u7F3A\u7701\u843D\u5728\u5DE6\u4E0B\u89D2 */\n@media (max-width: 767px) {\n .fa-widget {\n --fa-width: 140px;\n --fa-img-size: 112px;\n --fa-offset: 12px;\n padding: 13px;\n }\n\n /* \u89E6\u5C4F\u70B9\u6309\u533A\u57DF\u7A0D\u653E\u5927 */\n .fa-close {\n width: 24px;\n height: 24px;\n }\n}\n\n/* \u8DDF\u968F\u7CFB\u7EDF\u6DF1\u8272\u6A21\u5F0F \u2014\u2014 \u5FC5\u987B\u653E\u6587\u4EF6\u672B\u5C3E\uFF0C\u786E\u4FDD\u4F18\u5148\u7EA7\u9AD8\u4E8E\u4E0A\u9762\u7684\u6D45\u8272\u89C4\u5219\n (\u4EC5\u5F71\u54CD\u9ED8\u8BA4\u503C\uFF1B\u7528\u6237\u663E\u5F0F\u8BBE\u7F6E\u7684 --fa-* inline \u53D8\u91CF\u4ECD\u4F18\u5148) */\n@media (prefers-color-scheme: dark) {\n .fa-widget {\n --fa-bg: rgba(28, 28, 30, 0.96);\n --fa-accent: rgba(255, 255, 255, 0.92);\n --fa-border: rgba(255, 255, 255, 0.14);\n box-shadow: 0 2px 12px rgba(0, 0, 0, 0.5);\n }\n\n .fa-widget:hover {\n box-shadow: 0 6px 22px rgba(0, 0, 0, 0.6);\n }\n\n .fa-media {\n background: rgba(255, 255, 255, 0.06);\n }\n\n .fa-close {\n color: rgba(255, 255, 255, 0.4);\n }\n\n .fa-close:hover {\n color: rgba(255, 255, 255, 0.8);\n }\n\n .fa-desc {\n color: rgba(255, 255, 255, 0.55);\n }\n}\n';
206
+ var styles_default = '.fa-widget {\n --fa-bg: rgba(255, 255, 255, 0.96);\n --fa-accent: #333;\n --fa-radius: 12px;\n --fa-border: rgba(0, 0, 0, 0.1);\n --fa-offset: 16px;\n --fa-width: 150px;\n /* \u56FE\u7247\u8FB9\u957F\u4E0A\u9650\uFF1A\u9ED8\u8BA4 100% \u8BA9\u56FE\u7247\u94FA\u6EE1\u5361\u7247\uFF08\u65E0\u5185\u8FB9\u8DDD\uFF09\uFF0C\n \u663E\u5F0F\u4F20 imageSize / --fa-img-size \u65F6\u624D\u5C0F\u4E8E\u5361\u7247\u5BBD\u5EA6\u5E76\u5C45\u4E2D */\n --fa-img-size: 100%;\n --fa-img-fit: cover;\n\n position: fixed;\n z-index: 9999;\n box-sizing: border-box;\n width: var(--fa-width);\n padding: 0;\n overflow: hidden;\n background: var(--fa-bg);\n border: 1px solid var(--fa-border);\n border-radius: var(--fa-radius);\n box-shadow: 0 2px 10px rgba(0, 0, 0, 0.04);\n font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,\n "Helvetica Neue", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei",\n sans-serif;\n color: var(--fa-accent);\n line-height: 1.5;\n color-scheme: light dark;\n transition: box-shadow 0.2s ease;\n}\n\n.fa-widget:hover {\n box-shadow: 0 6px 20px rgba(0, 0, 0, 0.08);\n}\n\n.fa-widget[data-position="right-bottom"] {\n right: var(--fa-offset);\n bottom: var(--fa-offset);\n}\n\n.fa-widget[data-position="right-top"] {\n right: var(--fa-offset);\n top: var(--fa-offset);\n}\n\n.fa-widget[data-position="left-bottom"] {\n left: var(--fa-offset);\n bottom: var(--fa-offset);\n}\n\n.fa-widget[data-position="left-top"] {\n left: var(--fa-offset);\n top: var(--fa-offset);\n}\n\n.fa-widget[data-position="right-center"] {\n right: var(--fa-offset);\n top: 50%;\n transform: translateY(-50%);\n}\n\n.fa-widget[data-position="left-center"] {\n left: var(--fa-offset);\n top: 50%;\n transform: translateY(-50%);\n}\n\n.fa-close {\n position: absolute;\n top: 6px;\n right: 6px;\n z-index: 2;\n display: flex;\n align-items: center;\n justify-content: center;\n width: 22px;\n height: 22px;\n padding: 0;\n margin: 0;\n border: none;\n border-radius: 50%;\n /* \u56FE\u7247\u94FA\u6EE1\u5361\u7247\u540E\u6309\u94AE\u538B\u5728\u56FE\u4E0A\uFF0C\u7528\u534A\u900F\u660E\u5E95\u4FDD\u8BC1\u4EFB\u4F55\u56FE\u90FD\u80FD\u770B\u6E05 */\n background: rgba(0, 0, 0, 0.38);\n color: rgba(255, 255, 255, 0.95);\n backdrop-filter: blur(4px);\n -webkit-backdrop-filter: blur(4px);\n cursor: pointer;\n transition: background 0.15s ease, opacity 0.15s ease;\n}\n\n.fa-close:hover {\n background: rgba(0, 0, 0, 0.58);\n color: #fff;\n}\n\n/* \u89E6\u5C4F\u8BBE\u5907\uFF08\u65E0 hover\uFF09\uFF1A\u5173\u95ED\u6309\u94AE\u5E38\u663E\uFF0C\u4FDD\u8BC1\u53EF\u5173\u95ED */\n.fa-close {\n opacity: 1;\n}\n\n/* \u652F\u6301 hover \u7684\u8BBE\u5907\uFF08\u9F20\u6807\uFF09\uFF1A\u5E73\u65F6\u9690\u85CF\uFF0C\u60AC\u505C\u5361\u7247\u65F6\u6D6E\u73B0 */\n@media (hover: hover) and (pointer: fine) {\n .fa-close {\n opacity: 0;\n pointer-events: none;\n }\n .fa-widget:hover .fa-close,\n .fa-close:focus-visible {\n opacity: 1;\n pointer-events: auto;\n }\n}\n\n/* \u300C\u5E7F\u544A\u300D\u6807\u8BC6\uFF1A\u538B\u5728\u9996\u56FE\u5DE6\u4E0A\u89D2\uFF0C\u4E0D\u5360\u6392\u7248\u884C\u9AD8\n \uFF08\u300A\u5E7F\u544A\u6CD5\u300B\u8981\u6C42\u5E7F\u544A\u53EF\u8BC6\u522B\uFF0C\u6807\u8BC6\u4FDD\u7559\uFF0C\u4F46\u4E0D\u5FC5\u5355\u72EC\u5360\u4E00\u884C\uFF09 */\n.fa-label {\n position: absolute;\n top: 6px;\n left: 6px;\n z-index: 1;\n display: inline-block;\n font-size: 10px;\n line-height: 1;\n letter-spacing: 0.06em;\n padding: 3px 5px;\n border-radius: 4px;\n background: rgba(0, 0, 0, 0.45);\n color: rgba(255, 255, 255, 0.92);\n backdrop-filter: blur(4px);\n -webkit-backdrop-filter: blur(4px);\n pointer-events: none;\n}\n\n.fa-item {\n display: block;\n text-decoration: none;\n color: inherit;\n border-radius: 8px;\n}\n\na.fa-item {\n cursor: pointer;\n}\n\n/* \u56FE\u7247\u6B63\u65B9\u5F62\uFF0C\u9ED8\u8BA4\u94FA\u6EE1\u5361\u7247\u5BBD\u5EA6\uFF08\u5361\u7247\u65E0\u5185\u8FB9\u8DDD\uFF09\uFF1B\n \u663E\u5F0F\u4F20 imageSize \u65F6\u4F5C\u4E3A\u8FB9\u957F\u4E0A\u9650\uFF0C\u56FE\u7247\u5728\u5361\u7247\u5185\u5C45\u4E2D */\n.fa-media {\n position: relative;\n display: block;\n width: 100%;\n max-width: var(--fa-img-size, 100%);\n aspect-ratio: 1 / 1;\n margin: 0 auto;\n overflow: hidden;\n border-radius: 0;\n background: rgba(0, 0, 0, 0.04);\n}\n\n.fa-img {\n display: block;\n width: 100%;\n height: 100%;\n object-fit: var(--fa-img-fit, cover);\n transition: transform 0.25s ease;\n}\n\na.fa-item:hover .fa-img {\n transform: scale(1.04);\n}\n\n/* \u56FE\u7247\u52A0\u8F7D\u5931\u8D25\uFF1A\u9690\u85CF\u7A7A\u56FE\u6846\uFF0C\u53EA\u7559\u6587\u5B57\uFF1B\u8FDE\u6587\u5B57\u90FD\u6CA1\u6709\u5C31\u6574\u6761\u9690\u85CF */\n.fa-media--failed,\n.fa-item--failed {\n display: none;\n}\n\n.fa-tag {\n position: absolute;\n top: 6px;\n /* \u8BA9\u5F00\u5173\u95ED\u6309\u94AE\uFF08\u53F3\u4E0A\u89D2 6px + 22px\uFF09 */\n right: 34px;\n padding: 1px 6px;\n font-size: 10px;\n line-height: 1.5;\n border-radius: 999px;\n background: var(--fa-accent);\n color: var(--fa-bg);\n}\n\n/* \u5361\u7247\u5DF2\u65E0\u5185\u8FB9\u8DDD\uFF0C\u6587\u5B57\u533A\u81EA\u5E26\u5DE6\u53F3\u7559\u767D */\n.fa-body {\n display: block;\n padding: 8px 12px 12px;\n text-align: left;\n}\n\n.fa-item > .fa-body:first-child {\n padding-top: 12px;\n}\n\n.fa-title {\n display: block;\n font-size: 12px;\n font-weight: 500;\n color: var(--fa-accent);\n}\n\n.fa-desc {\n display: block;\n margin-top: 2px;\n font-size: 11px;\n color: rgba(0, 0, 0, 0.45);\n}\n\n.fa-divider {\n height: 1px;\n margin: 8px 12px;\n background: var(--fa-border);\n}\n\n/* \u79FB\u52A8\u7AEF\uFF1A\u6BD4\u684C\u9762\u7565\u5C0F\u4E00\u53F7\uFF08\u5361\u7247 140 / \u56FE\u7247\u94FA\u6EE1 / \u8DDD\u8FB9 12\uFF09\uFF0C\u7F3A\u7701\u540C\u6837\u843D\u5728\u5DE6\u4FA7\u5782\u76F4\u5C45\u4E2D */\n@media (max-width: 767px) {\n .fa-widget {\n --fa-width: 140px;\n --fa-offset: 12px;\n }\n\n /* \u89E6\u5C4F\u70B9\u6309\u533A\u57DF\u7A0D\u653E\u5927 */\n .fa-close {\n width: 24px;\n height: 24px;\n }\n}\n\n/* \u8DDF\u968F\u7CFB\u7EDF\u6DF1\u8272\u6A21\u5F0F \u2014\u2014 \u5FC5\u987B\u653E\u6587\u4EF6\u672B\u5C3E\uFF0C\u786E\u4FDD\u4F18\u5148\u7EA7\u9AD8\u4E8E\u4E0A\u9762\u7684\u6D45\u8272\u89C4\u5219\n (\u4EC5\u5F71\u54CD\u9ED8\u8BA4\u503C\uFF1B\u7528\u6237\u663E\u5F0F\u8BBE\u7F6E\u7684 --fa-* inline \u53D8\u91CF\u4ECD\u4F18\u5148) */\n@media (prefers-color-scheme: dark) {\n .fa-widget {\n --fa-bg: rgba(28, 28, 30, 0.96);\n --fa-accent: rgba(255, 255, 255, 0.92);\n --fa-border: rgba(255, 255, 255, 0.14);\n box-shadow: 0 2px 12px rgba(0, 0, 0, 0.5);\n }\n\n .fa-widget:hover {\n box-shadow: 0 6px 22px rgba(0, 0, 0, 0.6);\n }\n\n .fa-media {\n background: rgba(255, 255, 255, 0.06);\n }\n\n .fa-desc {\n color: rgba(255, 255, 255, 0.55);\n }\n}\n';
207
207
 
208
208
  // src/web-component.ts
209
209
  var TAG = "floating-ad";
package/dist/index.d.cts CHANGED
@@ -29,7 +29,7 @@ interface FloatingAdTheme {
29
29
  interface FloatingAdOptions {
30
30
  /** 广告列表(按数组顺序自上而下排列;为空时不渲染浮窗) */
31
31
  ads?: FloatingAdItem[];
32
- /** 浮窗位置,缺省:桌面 left-center / 移动端(<768px)left-bottom */
32
+ /** 浮窗位置,缺省:left-center(桌面与移动端一致,左侧垂直居中) */
33
33
  position?: Position;
34
34
  /** 关闭后是否记住状态(写入 localStorage),默认 false —— false 时刷新页面必然重新出现 */
35
35
  closePersistence?: boolean;
@@ -45,7 +45,7 @@ interface FloatingAdOptions {
45
45
  offset?: number;
46
46
  /** 图片填充方式,默认 cover */
47
47
  imageFit?: 'cover' | 'contain';
48
- /** 广告图片边长(px,正方形),缺省:桌面 120 / 移动端 112 */
48
+ /** 广告图片边长上限(px,正方形)。缺省铺满卡片宽度;设置后小于卡片宽度时居中 */
49
49
  imageSize?: number;
50
50
  /** 主题(映射为 CSS 变量,也可直接覆盖 --fa-* 变量) */
51
51
  theme?: FloatingAdTheme;
package/dist/index.d.ts CHANGED
@@ -29,7 +29,7 @@ interface FloatingAdTheme {
29
29
  interface FloatingAdOptions {
30
30
  /** 广告列表(按数组顺序自上而下排列;为空时不渲染浮窗) */
31
31
  ads?: FloatingAdItem[];
32
- /** 浮窗位置,缺省:桌面 left-center / 移动端(<768px)left-bottom */
32
+ /** 浮窗位置,缺省:left-center(桌面与移动端一致,左侧垂直居中) */
33
33
  position?: Position;
34
34
  /** 关闭后是否记住状态(写入 localStorage),默认 false —— false 时刷新页面必然重新出现 */
35
35
  closePersistence?: boolean;
@@ -45,7 +45,7 @@ interface FloatingAdOptions {
45
45
  offset?: number;
46
46
  /** 图片填充方式,默认 cover */
47
47
  imageFit?: 'cover' | 'contain';
48
- /** 广告图片边长(px,正方形),缺省:桌面 120 / 移动端 112 */
48
+ /** 广告图片边长上限(px,正方形)。缺省铺满卡片宽度;设置后小于卡片宽度时居中 */
49
49
  imageSize?: number;
50
50
  /** 主题(映射为 CSS 变量,也可直接覆盖 --fa-* 变量) */
51
51
  theme?: FloatingAdTheme;
package/dist/index.esm.js CHANGED
@@ -36,8 +36,8 @@ function resolveOptions(options) {
36
36
  const raw = Array.isArray(options.ads) ? options.ads : [];
37
37
  return {
38
38
  ads: raw.filter(isRenderableAd),
39
- // 移动端缺省落在左下角(不挡右侧阅读区/返回顶部按钮)
40
- position: (_a = options.position) != null ? _a : isMobile() ? "left-bottom" : "left-center",
39
+ // 桌面与移动端缺省一致:左侧垂直居中(不挡正文,也不压右下角的返回顶部按钮)
40
+ position: (_a = options.position) != null ? _a : "left-center",
41
41
  closePersistence: (_b = options.closePersistence) != null ? _b : false,
42
42
  hideOnMobile: (_c = options.hideOnMobile) != null ? _c : false,
43
43
  zIndex: (_d = options.zIndex) != null ? _d : 9999,
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/FloatingAd.ts","../src/index.ts"],"sourcesContent":["export type Position =\n | 'right-bottom'\n | 'right-top'\n | 'right-center'\n | 'left-bottom'\n | 'left-top'\n | 'left-center'\n\n/** 单条广告 */\nexport interface FloatingAdItem {\n /** 广告图片地址(必填;没有图片的条目不会渲染) */\n src: string\n /** 标题 */\n title?: string\n /** 副文案 / 卖点 */\n desc?: string\n /** 点击跳转链接(可选,不传则不可点击) */\n href?: string\n /** 跳转方式,默认 `_blank`(仅在传了 href 时生效) */\n target?: '_blank' | '_self'\n /** 图片 alt,缺省使用标题 */\n alt?: string\n /** 图片右上角角标文案,如「限时」 */\n badge?: string\n}\n\nexport interface FloatingAdTheme {\n /** 浮窗背景色 */\n bg?: string\n /** 强调色(标题、角标) */\n accent?: string\n /** 圆角大小 */\n radius?: string\n /** 边框颜色 */\n border?: string\n}\n\nexport interface FloatingAdOptions {\n /** 广告列表(按数组顺序自上而下排列;为空时不渲染浮窗) */\n ads?: FloatingAdItem[]\n /** 浮窗位置,缺省:桌面 left-center / 移动端(<768px)left-bottom */\n position?: Position\n /** 关闭后是否记住状态(写入 localStorage),默认 false —— false 时刷新页面必然重新出现 */\n closePersistence?: boolean\n /** 移动端(<768px)是否隐藏,默认 false(移动端展示更小一号的浮窗) */\n hideOnMobile?: boolean\n /** 浮窗 z-index,默认 9999 */\n zIndex?: number\n /** 「广告」标识文案(压在首图左上角),默认 false 不显示;传字符串则显示该文案 */\n adLabel?: string | false\n /** 浮窗宽度(px),缺省:桌面 150 / 移动端 140 */\n width?: number\n /** 浮窗距视口边缘距离(px),缺省:桌面 16 / 移动端 12 */\n offset?: number\n /** 图片填充方式,默认 cover */\n imageFit?: 'cover' | 'contain'\n /** 广告图片边长(px,正方形),缺省:桌面 120 / 移动端 112 */\n imageSize?: number\n /** 主题(映射为 CSS 变量,也可直接覆盖 --fa-* 变量) */\n theme?: FloatingAdTheme\n /** 点击广告回调(不影响默认跳转行为) */\n onClick?: (ad: FloatingAdItem, event: MouseEvent) => void\n}\n\ninterface ResolvedOptions {\n ads: FloatingAdItem[]\n position: Position\n closePersistence: boolean\n hideOnMobile: boolean\n zIndex: number\n adLabel: string | false\n imageFit: 'cover' | 'contain'\n /** 用户显式传入的尺寸子集(未配置时不写 inline,交给 CSS 按桌面/移动端默认值走) */\n sizeOverrides: {\n width?: number\n offset?: number\n imageSize?: number\n }\n theme: Required<FloatingAdTheme>\n /** 用户显式传入的主题子集(未配置的 key 交给 CSS 默认值,可随系统深浅色变化) */\n themeOverrides: FloatingAdTheme\n onClick?: (ad: FloatingAdItem, event: MouseEvent) => void\n}\n\nconst DEFAULT_THEME: Required<FloatingAdTheme> = {\n bg: 'rgba(255, 255, 255, 0.96)',\n accent: '#333',\n radius: '12px',\n border: 'rgba(0, 0, 0, 0.1)'\n}\n\nconst STORAGE_KEY = 'floating-ad:closed'\nconst MOBILE_QUERY = '(max-width: 767px)'\n\nfunction isMobile(): boolean {\n if (typeof window === 'undefined') return false\n return window.matchMedia?.(MOBILE_QUERY)?.matches ?? false\n}\n\nfunction hasCloseMark(): boolean {\n if (typeof localStorage === 'undefined') return false\n try {\n return localStorage.getItem(STORAGE_KEY) === '1'\n } catch {\n return false\n }\n}\n\nfunction setCloseMark(): void {\n if (typeof localStorage === 'undefined') return\n try {\n localStorage.setItem(STORAGE_KEY, '1')\n } catch {\n /* ignore quota / privacy mode */\n }\n}\n\n/** 只有带图片的条目才会渲染(本组件只承载图片广告) */\nfunction isRenderableAd(ad: FloatingAdItem | null | undefined): ad is FloatingAdItem {\n return Boolean(ad && typeof ad === 'object' && ad.src?.trim())\n}\n\nfunction resolveOptions(options: FloatingAdOptions): ResolvedOptions {\n const raw = Array.isArray(options.ads) ? options.ads : []\n return {\n ads: raw.filter(isRenderableAd),\n // 移动端缺省落在左下角(不挡右侧阅读区/返回顶部按钮)\n position: options.position ?? (isMobile() ? 'left-bottom' : 'left-center'),\n closePersistence: options.closePersistence ?? false,\n hideOnMobile: options.hideOnMobile ?? false,\n zIndex: options.zIndex ?? 9999,\n adLabel: options.adLabel === undefined ? false : options.adLabel,\n imageFit: options.imageFit === 'contain' ? 'contain' : 'cover',\n sizeOverrides: {\n width: Number.isFinite(options.width) ? Math.max(80, Number(options.width)) : undefined,\n offset: Number.isFinite(options.offset) ? Math.max(0, Number(options.offset)) : undefined,\n imageSize: Number.isFinite(options.imageSize)\n ? Math.max(40, Number(options.imageSize))\n : undefined\n },\n theme: { ...DEFAULT_THEME, ...(options.theme ?? {}) },\n themeOverrides: options.theme ?? {},\n onClick: options.onClick\n }\n}\n\n/**\n * A minimal floating ad slot that pins image / text ads to the left (or right) edge.\n * Zero dependencies. Framework-agnostic. Styleable via --fa-* CSS variables.\n *\n * 没有广告数据时不渲染任何节点(不会留下空白卡片)。\n *\n * @param container 渲染容器,默认 document.body;Web Component 场景传入 shadow root\n */\nexport class FloatingAd {\n private opts: ResolvedOptions\n private readonly container: HTMLElement | ShadowRoot\n private el: HTMLElement | null = null\n private closeBtn: HTMLButtonElement | null = null\n /** 本次渲染的广告列表(与 DOM 上的 data-index 一一对应) */\n private rendered: FloatingAdItem[] = []\n\n constructor(\n options: FloatingAdOptions = {},\n container: HTMLElement | ShadowRoot = document.body\n ) {\n this.opts = resolveOptions(options)\n this.container = container\n\n if (this.opts.hideOnMobile && isMobile()) return\n if (this.opts.closePersistence && hasCloseMark()) return\n\n this.render()\n }\n\n /** 是否已挂载到页面 */\n isMounted(): boolean {\n return this.el !== null && this.el.isConnected\n }\n\n /** 打开(已关闭后再次显示,忽略 localStorage 关闭标记) */\n open(): void {\n if (this.isMounted()) return\n this.render()\n }\n\n /** 关闭浮窗(完全移除)。默认不记住状态,刷新后重新出现 */\n close(): void {\n if (this.opts.closePersistence) {\n setCloseMark()\n }\n this.destroy()\n }\n\n /** 从页面移除并解绑 */\n destroy(): void {\n this.closeBtn?.removeEventListener('click', this.handleClose)\n this.closeBtn = null\n this.el?.removeEventListener('click', this.handleItemClick)\n this.el?.removeEventListener('error', this.handleImageError, true)\n this.el?.remove()\n this.el = null\n this.rendered = []\n }\n\n /** 用新配置重渲染(保留原容器) */\n update(options: FloatingAdOptions): void {\n this.destroy()\n this.opts = resolveOptions(options)\n\n if (this.opts.hideOnMobile && isMobile()) return\n if (this.opts.closePersistence && hasCloseMark()) return\n\n this.render()\n }\n\n private render(): void {\n const { ads, position, zIndex, themeOverrides, adLabel, imageFit, sizeOverrides } = this.opts\n\n if (ads.length === 0) return\n\n const root = document.createElement('div')\n root.className = 'fa-widget'\n root.dataset.position = position\n root.style.zIndex = String(zIndex)\n root.style.setProperty('--fa-img-fit', imageFit)\n // 尺寸只在用户显式配置时写 inline,其余交给 CSS(桌面 / 移动端各一套默认值)\n if (sizeOverrides.width !== undefined) {\n root.style.setProperty('--fa-width', `${sizeOverrides.width}px`)\n }\n if (sizeOverrides.offset !== undefined) {\n root.style.setProperty('--fa-offset', `${sizeOverrides.offset}px`)\n }\n if (sizeOverrides.imageSize !== undefined) {\n root.style.setProperty('--fa-img-size', `${sizeOverrides.imageSize}px`)\n }\n\n // 仅把用户显式设置的主题写为 inline(用户优先);\n // 未设置的 key 交给 CSS 变量默认值,以便跟随系统深浅色(prefers-color-scheme)\n const overrides = [\n ['--fa-bg', themeOverrides.bg],\n ['--fa-accent', themeOverrides.accent],\n ['--fa-radius', themeOverrides.radius],\n ['--fa-border', themeOverrides.border]\n ] as const\n for (const [key, value] of overrides) {\n if (value !== undefined) root.style.setProperty(key, value)\n }\n\n // 「广告」标识不单独占行:压在首图左上角\n const label = adLabel ? escapeHtml(String(adLabel)) : ''\n\n root.innerHTML = `\n <button class=\"fa-close\" type=\"button\" aria-label=\"关闭广告\">${CLOSE_SVG}</button>\n ${ads\n .map((ad, index) => this.renderItem(ad, index, index === 0 ? label : ''))\n .join('<div class=\"fa-divider\" role=\"separator\"></div>')}\n `\n\n this.closeBtn = root.querySelector<HTMLButtonElement>('.fa-close')\n this.closeBtn?.addEventListener('click', this.handleClose)\n root.addEventListener('click', this.handleItemClick)\n // 图片加载失败时降级:隐藏空图框,仅保留文字;连文字都没有就整条隐藏\n root.addEventListener('error', this.handleImageError, true)\n\n this.rendered = ads\n this.container.appendChild(root)\n this.el = root\n }\n\n private renderItem(ad: FloatingAdItem, index: number, label: string): string {\n const media = `<span class=\"fa-media\">\n <img class=\"fa-img\" src=\"${escapeAttr(ad.src)}\" alt=\"${escapeAttr(ad.alt ?? ad.title ?? '')}\" loading=\"lazy\" decoding=\"async\" />\n ${ad.badge ? `<span class=\"fa-tag\">${escapeHtml(ad.badge)}</span>` : ''}\n ${label ? `<span class=\"fa-label\">${label}</span>` : ''}\n </span>`\n\n const body =\n ad.title || ad.desc\n ? `<span class=\"fa-body\">\n ${ad.title ? `<span class=\"fa-title\">${escapeHtml(ad.title)}</span>` : ''}\n ${ad.desc ? `<span class=\"fa-desc\">${escapeHtml(ad.desc)}</span>` : ''}\n </span>`\n : ''\n\n const inner = `${media}${body}`\n\n const href = ad.href ? safeHref(ad.href) : null\n if (href) {\n const target = ad.target === '_self' ? '_self' : '_blank'\n const rel =\n target === '_blank'\n ? ' rel=\"noopener noreferrer nofollow sponsored\"'\n : ' rel=\"nofollow sponsored\"'\n return `<a class=\"fa-item\" href=\"${escapeAttr(href)}\" target=\"${target}\"${rel} data-index=\"${index}\">${inner}</a>`\n }\n\n return `<div class=\"fa-item\" data-index=\"${index}\">${inner}</div>`\n }\n\n private readonly handleClose = (): void => {\n this.close()\n }\n\n private readonly handleImageError = (event: Event): void => {\n const img = event.target as HTMLElement | null\n if (!img || img.tagName !== 'IMG' || !img.classList.contains('fa-img')) return\n const media = img.closest('.fa-media')\n if (!media) return\n media.classList.add('fa-media--failed')\n const item = media.closest('.fa-item')\n if (item && !item.querySelector('.fa-body')) item.classList.add('fa-item--failed')\n }\n\n private readonly handleItemClick = (event: Event): void => {\n if (!this.opts.onClick) return\n const target = event.target as Element | null\n const item = target?.closest?.('.fa-item') as HTMLElement | null\n if (!item) return\n const index = Number(item.dataset.index)\n const ad = this.rendered[index]\n if (!ad) return\n this.opts.onClick(ad, event as MouseEvent)\n }\n}\n\nconst CLOSE_SVG =\n '<svg viewBox=\"0 0 24 24\" width=\"14\" height=\"14\" aria-hidden=\"true\" focusable=\"false\"><path d=\"M6 6l12 12M18 6L6 18\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"/></svg>'\n\n/** 过滤危险协议(javascript: / data: / vbscript: 等),返回可用链接或 null */\nfunction safeHref(href: string): string | null {\n const trimmed = href.trim()\n if (!trimmed) return null\n const compact = trimmed.replace(/[\\s\\u0000-\\u001f]/g, '').toLowerCase()\n if (/^(javascript|data|vbscript|file):/.test(compact)) return null\n return trimmed\n}\n\nfunction escapeHtml(value: string): string {\n return value\n .replace(/&/g, '&amp;')\n .replace(/</g, '&lt;')\n .replace(/>/g, '&gt;')\n .replace(/\"/g, '&quot;')\n .replace(/'/g, '&#39;')\n}\n\nfunction escapeAttr(value: string): string {\n return escapeHtml(value)\n}\n","import { FloatingAd } from './FloatingAd'\n\nexport { FloatingAd }\nexport default FloatingAd\nexport type { FloatingAdOptions, FloatingAdItem, FloatingAdTheme, Position } from './FloatingAd'\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/FloatingAd.ts","../src/index.ts"],"sourcesContent":["export type Position =\n | 'right-bottom'\n | 'right-top'\n | 'right-center'\n | 'left-bottom'\n | 'left-top'\n | 'left-center'\n\n/** 单条广告 */\nexport interface FloatingAdItem {\n /** 广告图片地址(必填;没有图片的条目不会渲染) */\n src: string\n /** 标题 */\n title?: string\n /** 副文案 / 卖点 */\n desc?: string\n /** 点击跳转链接(可选,不传则不可点击) */\n href?: string\n /** 跳转方式,默认 `_blank`(仅在传了 href 时生效) */\n target?: '_blank' | '_self'\n /** 图片 alt,缺省使用标题 */\n alt?: string\n /** 图片右上角角标文案,如「限时」 */\n badge?: string\n}\n\nexport interface FloatingAdTheme {\n /** 浮窗背景色 */\n bg?: string\n /** 强调色(标题、角标) */\n accent?: string\n /** 圆角大小 */\n radius?: string\n /** 边框颜色 */\n border?: string\n}\n\nexport interface FloatingAdOptions {\n /** 广告列表(按数组顺序自上而下排列;为空时不渲染浮窗) */\n ads?: FloatingAdItem[]\n /** 浮窗位置,缺省:left-center(桌面与移动端一致,左侧垂直居中) */\n position?: Position\n /** 关闭后是否记住状态(写入 localStorage),默认 false —— false 时刷新页面必然重新出现 */\n closePersistence?: boolean\n /** 移动端(<768px)是否隐藏,默认 false(移动端展示更小一号的浮窗) */\n hideOnMobile?: boolean\n /** 浮窗 z-index,默认 9999 */\n zIndex?: number\n /** 「广告」标识文案(压在首图左上角),默认 false 不显示;传字符串则显示该文案 */\n adLabel?: string | false\n /** 浮窗宽度(px),缺省:桌面 150 / 移动端 140 */\n width?: number\n /** 浮窗距视口边缘距离(px),缺省:桌面 16 / 移动端 12 */\n offset?: number\n /** 图片填充方式,默认 cover */\n imageFit?: 'cover' | 'contain'\n /** 广告图片边长上限(px,正方形)。缺省铺满卡片宽度;设置后小于卡片宽度时居中 */\n imageSize?: number\n /** 主题(映射为 CSS 变量,也可直接覆盖 --fa-* 变量) */\n theme?: FloatingAdTheme\n /** 点击广告回调(不影响默认跳转行为) */\n onClick?: (ad: FloatingAdItem, event: MouseEvent) => void\n}\n\ninterface ResolvedOptions {\n ads: FloatingAdItem[]\n position: Position\n closePersistence: boolean\n hideOnMobile: boolean\n zIndex: number\n adLabel: string | false\n imageFit: 'cover' | 'contain'\n /** 用户显式传入的尺寸子集(未配置时不写 inline,交给 CSS 按桌面/移动端默认值走) */\n sizeOverrides: {\n width?: number\n offset?: number\n imageSize?: number\n }\n theme: Required<FloatingAdTheme>\n /** 用户显式传入的主题子集(未配置的 key 交给 CSS 默认值,可随系统深浅色变化) */\n themeOverrides: FloatingAdTheme\n onClick?: (ad: FloatingAdItem, event: MouseEvent) => void\n}\n\nconst DEFAULT_THEME: Required<FloatingAdTheme> = {\n bg: 'rgba(255, 255, 255, 0.96)',\n accent: '#333',\n radius: '12px',\n border: 'rgba(0, 0, 0, 0.1)'\n}\n\nconst STORAGE_KEY = 'floating-ad:closed'\nconst MOBILE_QUERY = '(max-width: 767px)'\n\nfunction isMobile(): boolean {\n if (typeof window === 'undefined') return false\n return window.matchMedia?.(MOBILE_QUERY)?.matches ?? false\n}\n\nfunction hasCloseMark(): boolean {\n if (typeof localStorage === 'undefined') return false\n try {\n return localStorage.getItem(STORAGE_KEY) === '1'\n } catch {\n return false\n }\n}\n\nfunction setCloseMark(): void {\n if (typeof localStorage === 'undefined') return\n try {\n localStorage.setItem(STORAGE_KEY, '1')\n } catch {\n /* ignore quota / privacy mode */\n }\n}\n\n/** 只有带图片的条目才会渲染(本组件只承载图片广告) */\nfunction isRenderableAd(ad: FloatingAdItem | null | undefined): ad is FloatingAdItem {\n return Boolean(ad && typeof ad === 'object' && ad.src?.trim())\n}\n\nfunction resolveOptions(options: FloatingAdOptions): ResolvedOptions {\n const raw = Array.isArray(options.ads) ? options.ads : []\n return {\n ads: raw.filter(isRenderableAd),\n // 桌面与移动端缺省一致:左侧垂直居中(不挡正文,也不压右下角的返回顶部按钮)\n position: options.position ?? 'left-center',\n closePersistence: options.closePersistence ?? false,\n hideOnMobile: options.hideOnMobile ?? false,\n zIndex: options.zIndex ?? 9999,\n adLabel: options.adLabel === undefined ? false : options.adLabel,\n imageFit: options.imageFit === 'contain' ? 'contain' : 'cover',\n sizeOverrides: {\n width: Number.isFinite(options.width) ? Math.max(80, Number(options.width)) : undefined,\n offset: Number.isFinite(options.offset) ? Math.max(0, Number(options.offset)) : undefined,\n imageSize: Number.isFinite(options.imageSize)\n ? Math.max(40, Number(options.imageSize))\n : undefined\n },\n theme: { ...DEFAULT_THEME, ...(options.theme ?? {}) },\n themeOverrides: options.theme ?? {},\n onClick: options.onClick\n }\n}\n\n/**\n * A minimal floating ad slot that pins image / text ads to the left (or right) edge.\n * Zero dependencies. Framework-agnostic. Styleable via --fa-* CSS variables.\n *\n * 没有广告数据时不渲染任何节点(不会留下空白卡片)。\n *\n * @param container 渲染容器,默认 document.body;Web Component 场景传入 shadow root\n */\nexport class FloatingAd {\n private opts: ResolvedOptions\n private readonly container: HTMLElement | ShadowRoot\n private el: HTMLElement | null = null\n private closeBtn: HTMLButtonElement | null = null\n /** 本次渲染的广告列表(与 DOM 上的 data-index 一一对应) */\n private rendered: FloatingAdItem[] = []\n\n constructor(\n options: FloatingAdOptions = {},\n container: HTMLElement | ShadowRoot = document.body\n ) {\n this.opts = resolveOptions(options)\n this.container = container\n\n if (this.opts.hideOnMobile && isMobile()) return\n if (this.opts.closePersistence && hasCloseMark()) return\n\n this.render()\n }\n\n /** 是否已挂载到页面 */\n isMounted(): boolean {\n return this.el !== null && this.el.isConnected\n }\n\n /** 打开(已关闭后再次显示,忽略 localStorage 关闭标记) */\n open(): void {\n if (this.isMounted()) return\n this.render()\n }\n\n /** 关闭浮窗(完全移除)。默认不记住状态,刷新后重新出现 */\n close(): void {\n if (this.opts.closePersistence) {\n setCloseMark()\n }\n this.destroy()\n }\n\n /** 从页面移除并解绑 */\n destroy(): void {\n this.closeBtn?.removeEventListener('click', this.handleClose)\n this.closeBtn = null\n this.el?.removeEventListener('click', this.handleItemClick)\n this.el?.removeEventListener('error', this.handleImageError, true)\n this.el?.remove()\n this.el = null\n this.rendered = []\n }\n\n /** 用新配置重渲染(保留原容器) */\n update(options: FloatingAdOptions): void {\n this.destroy()\n this.opts = resolveOptions(options)\n\n if (this.opts.hideOnMobile && isMobile()) return\n if (this.opts.closePersistence && hasCloseMark()) return\n\n this.render()\n }\n\n private render(): void {\n const { ads, position, zIndex, themeOverrides, adLabel, imageFit, sizeOverrides } = this.opts\n\n if (ads.length === 0) return\n\n const root = document.createElement('div')\n root.className = 'fa-widget'\n root.dataset.position = position\n root.style.zIndex = String(zIndex)\n root.style.setProperty('--fa-img-fit', imageFit)\n // 尺寸只在用户显式配置时写 inline,其余交给 CSS(桌面 / 移动端各一套默认值)\n if (sizeOverrides.width !== undefined) {\n root.style.setProperty('--fa-width', `${sizeOverrides.width}px`)\n }\n if (sizeOverrides.offset !== undefined) {\n root.style.setProperty('--fa-offset', `${sizeOverrides.offset}px`)\n }\n if (sizeOverrides.imageSize !== undefined) {\n root.style.setProperty('--fa-img-size', `${sizeOverrides.imageSize}px`)\n }\n\n // 仅把用户显式设置的主题写为 inline(用户优先);\n // 未设置的 key 交给 CSS 变量默认值,以便跟随系统深浅色(prefers-color-scheme)\n const overrides = [\n ['--fa-bg', themeOverrides.bg],\n ['--fa-accent', themeOverrides.accent],\n ['--fa-radius', themeOverrides.radius],\n ['--fa-border', themeOverrides.border]\n ] as const\n for (const [key, value] of overrides) {\n if (value !== undefined) root.style.setProperty(key, value)\n }\n\n // 「广告」标识不单独占行:压在首图左上角\n const label = adLabel ? escapeHtml(String(adLabel)) : ''\n\n root.innerHTML = `\n <button class=\"fa-close\" type=\"button\" aria-label=\"关闭广告\">${CLOSE_SVG}</button>\n ${ads\n .map((ad, index) => this.renderItem(ad, index, index === 0 ? label : ''))\n .join('<div class=\"fa-divider\" role=\"separator\"></div>')}\n `\n\n this.closeBtn = root.querySelector<HTMLButtonElement>('.fa-close')\n this.closeBtn?.addEventListener('click', this.handleClose)\n root.addEventListener('click', this.handleItemClick)\n // 图片加载失败时降级:隐藏空图框,仅保留文字;连文字都没有就整条隐藏\n root.addEventListener('error', this.handleImageError, true)\n\n this.rendered = ads\n this.container.appendChild(root)\n this.el = root\n }\n\n private renderItem(ad: FloatingAdItem, index: number, label: string): string {\n const media = `<span class=\"fa-media\">\n <img class=\"fa-img\" src=\"${escapeAttr(ad.src)}\" alt=\"${escapeAttr(ad.alt ?? ad.title ?? '')}\" loading=\"lazy\" decoding=\"async\" />\n ${ad.badge ? `<span class=\"fa-tag\">${escapeHtml(ad.badge)}</span>` : ''}\n ${label ? `<span class=\"fa-label\">${label}</span>` : ''}\n </span>`\n\n const body =\n ad.title || ad.desc\n ? `<span class=\"fa-body\">\n ${ad.title ? `<span class=\"fa-title\">${escapeHtml(ad.title)}</span>` : ''}\n ${ad.desc ? `<span class=\"fa-desc\">${escapeHtml(ad.desc)}</span>` : ''}\n </span>`\n : ''\n\n const inner = `${media}${body}`\n\n const href = ad.href ? safeHref(ad.href) : null\n if (href) {\n const target = ad.target === '_self' ? '_self' : '_blank'\n const rel =\n target === '_blank'\n ? ' rel=\"noopener noreferrer nofollow sponsored\"'\n : ' rel=\"nofollow sponsored\"'\n return `<a class=\"fa-item\" href=\"${escapeAttr(href)}\" target=\"${target}\"${rel} data-index=\"${index}\">${inner}</a>`\n }\n\n return `<div class=\"fa-item\" data-index=\"${index}\">${inner}</div>`\n }\n\n private readonly handleClose = (): void => {\n this.close()\n }\n\n private readonly handleImageError = (event: Event): void => {\n const img = event.target as HTMLElement | null\n if (!img || img.tagName !== 'IMG' || !img.classList.contains('fa-img')) return\n const media = img.closest('.fa-media')\n if (!media) return\n media.classList.add('fa-media--failed')\n const item = media.closest('.fa-item')\n if (item && !item.querySelector('.fa-body')) item.classList.add('fa-item--failed')\n }\n\n private readonly handleItemClick = (event: Event): void => {\n if (!this.opts.onClick) return\n const target = event.target as Element | null\n const item = target?.closest?.('.fa-item') as HTMLElement | null\n if (!item) return\n const index = Number(item.dataset.index)\n const ad = this.rendered[index]\n if (!ad) return\n this.opts.onClick(ad, event as MouseEvent)\n }\n}\n\nconst CLOSE_SVG =\n '<svg viewBox=\"0 0 24 24\" width=\"14\" height=\"14\" aria-hidden=\"true\" focusable=\"false\"><path d=\"M6 6l12 12M18 6L6 18\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"/></svg>'\n\n/** 过滤危险协议(javascript: / data: / vbscript: 等),返回可用链接或 null */\nfunction safeHref(href: string): string | null {\n const trimmed = href.trim()\n if (!trimmed) return null\n const compact = trimmed.replace(/[\\s\\u0000-\\u001f]/g, '').toLowerCase()\n if (/^(javascript|data|vbscript|file):/.test(compact)) return null\n return trimmed\n}\n\nfunction escapeHtml(value: string): string {\n return value\n .replace(/&/g, '&amp;')\n .replace(/</g, '&lt;')\n .replace(/>/g, '&gt;')\n .replace(/\"/g, '&quot;')\n .replace(/'/g, '&#39;')\n}\n\nfunction escapeAttr(value: string): string {\n return escapeHtml(value)\n}\n","import { FloatingAd } from './FloatingAd'\n\nexport { FloatingAd }\nexport default FloatingAd\nexport type { FloatingAdOptions, FloatingAdItem, FloatingAdTheme, Position } from './FloatingAd'\n"],"mappings":"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","names":[]}
@@ -63,8 +63,8 @@ function resolveOptions(options) {
63
63
  const raw = Array.isArray(options.ads) ? options.ads : [];
64
64
  return {
65
65
  ads: raw.filter(isRenderableAd),
66
- // 移动端缺省落在左下角(不挡右侧阅读区/返回顶部按钮)
67
- position: (_a = options.position) != null ? _a : isMobile() ? "left-bottom" : "left-center",
66
+ // 桌面与移动端缺省一致:左侧垂直居中(不挡正文,也不压右下角的返回顶部按钮)
67
+ position: (_a = options.position) != null ? _a : "left-center",
68
68
  closePersistence: (_b = options.closePersistence) != null ? _b : false,
69
69
  hideOnMobile: (_c = options.hideOnMobile) != null ? _c : false,
70
70
  zIndex: (_d = options.zIndex) != null ? _d : 9999,
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/FloatingAd.ts"],"sourcesContent":["import { FloatingAd } from './FloatingAd'\n\nexport { FloatingAd }\nexport default FloatingAd\nexport type { FloatingAdOptions, FloatingAdItem, FloatingAdTheme, Position } from './FloatingAd'\n","export type Position =\n | 'right-bottom'\n | 'right-top'\n | 'right-center'\n | 'left-bottom'\n | 'left-top'\n | 'left-center'\n\n/** 单条广告 */\nexport interface FloatingAdItem {\n /** 广告图片地址(必填;没有图片的条目不会渲染) */\n src: string\n /** 标题 */\n title?: string\n /** 副文案 / 卖点 */\n desc?: string\n /** 点击跳转链接(可选,不传则不可点击) */\n href?: string\n /** 跳转方式,默认 `_blank`(仅在传了 href 时生效) */\n target?: '_blank' | '_self'\n /** 图片 alt,缺省使用标题 */\n alt?: string\n /** 图片右上角角标文案,如「限时」 */\n badge?: string\n}\n\nexport interface FloatingAdTheme {\n /** 浮窗背景色 */\n bg?: string\n /** 强调色(标题、角标) */\n accent?: string\n /** 圆角大小 */\n radius?: string\n /** 边框颜色 */\n border?: string\n}\n\nexport interface FloatingAdOptions {\n /** 广告列表(按数组顺序自上而下排列;为空时不渲染浮窗) */\n ads?: FloatingAdItem[]\n /** 浮窗位置,缺省:桌面 left-center / 移动端(<768px)left-bottom */\n position?: Position\n /** 关闭后是否记住状态(写入 localStorage),默认 false —— false 时刷新页面必然重新出现 */\n closePersistence?: boolean\n /** 移动端(<768px)是否隐藏,默认 false(移动端展示更小一号的浮窗) */\n hideOnMobile?: boolean\n /** 浮窗 z-index,默认 9999 */\n zIndex?: number\n /** 「广告」标识文案(压在首图左上角),默认 false 不显示;传字符串则显示该文案 */\n adLabel?: string | false\n /** 浮窗宽度(px),缺省:桌面 150 / 移动端 140 */\n width?: number\n /** 浮窗距视口边缘距离(px),缺省:桌面 16 / 移动端 12 */\n offset?: number\n /** 图片填充方式,默认 cover */\n imageFit?: 'cover' | 'contain'\n /** 广告图片边长(px,正方形),缺省:桌面 120 / 移动端 112 */\n imageSize?: number\n /** 主题(映射为 CSS 变量,也可直接覆盖 --fa-* 变量) */\n theme?: FloatingAdTheme\n /** 点击广告回调(不影响默认跳转行为) */\n onClick?: (ad: FloatingAdItem, event: MouseEvent) => void\n}\n\ninterface ResolvedOptions {\n ads: FloatingAdItem[]\n position: Position\n closePersistence: boolean\n hideOnMobile: boolean\n zIndex: number\n adLabel: string | false\n imageFit: 'cover' | 'contain'\n /** 用户显式传入的尺寸子集(未配置时不写 inline,交给 CSS 按桌面/移动端默认值走) */\n sizeOverrides: {\n width?: number\n offset?: number\n imageSize?: number\n }\n theme: Required<FloatingAdTheme>\n /** 用户显式传入的主题子集(未配置的 key 交给 CSS 默认值,可随系统深浅色变化) */\n themeOverrides: FloatingAdTheme\n onClick?: (ad: FloatingAdItem, event: MouseEvent) => void\n}\n\nconst DEFAULT_THEME: Required<FloatingAdTheme> = {\n bg: 'rgba(255, 255, 255, 0.96)',\n accent: '#333',\n radius: '12px',\n border: 'rgba(0, 0, 0, 0.1)'\n}\n\nconst STORAGE_KEY = 'floating-ad:closed'\nconst MOBILE_QUERY = '(max-width: 767px)'\n\nfunction isMobile(): boolean {\n if (typeof window === 'undefined') return false\n return window.matchMedia?.(MOBILE_QUERY)?.matches ?? false\n}\n\nfunction hasCloseMark(): boolean {\n if (typeof localStorage === 'undefined') return false\n try {\n return localStorage.getItem(STORAGE_KEY) === '1'\n } catch {\n return false\n }\n}\n\nfunction setCloseMark(): void {\n if (typeof localStorage === 'undefined') return\n try {\n localStorage.setItem(STORAGE_KEY, '1')\n } catch {\n /* ignore quota / privacy mode */\n }\n}\n\n/** 只有带图片的条目才会渲染(本组件只承载图片广告) */\nfunction isRenderableAd(ad: FloatingAdItem | null | undefined): ad is FloatingAdItem {\n return Boolean(ad && typeof ad === 'object' && ad.src?.trim())\n}\n\nfunction resolveOptions(options: FloatingAdOptions): ResolvedOptions {\n const raw = Array.isArray(options.ads) ? options.ads : []\n return {\n ads: raw.filter(isRenderableAd),\n // 移动端缺省落在左下角(不挡右侧阅读区/返回顶部按钮)\n position: options.position ?? (isMobile() ? 'left-bottom' : 'left-center'),\n closePersistence: options.closePersistence ?? false,\n hideOnMobile: options.hideOnMobile ?? false,\n zIndex: options.zIndex ?? 9999,\n adLabel: options.adLabel === undefined ? false : options.adLabel,\n imageFit: options.imageFit === 'contain' ? 'contain' : 'cover',\n sizeOverrides: {\n width: Number.isFinite(options.width) ? Math.max(80, Number(options.width)) : undefined,\n offset: Number.isFinite(options.offset) ? Math.max(0, Number(options.offset)) : undefined,\n imageSize: Number.isFinite(options.imageSize)\n ? Math.max(40, Number(options.imageSize))\n : undefined\n },\n theme: { ...DEFAULT_THEME, ...(options.theme ?? {}) },\n themeOverrides: options.theme ?? {},\n onClick: options.onClick\n }\n}\n\n/**\n * A minimal floating ad slot that pins image / text ads to the left (or right) edge.\n * Zero dependencies. Framework-agnostic. Styleable via --fa-* CSS variables.\n *\n * 没有广告数据时不渲染任何节点(不会留下空白卡片)。\n *\n * @param container 渲染容器,默认 document.body;Web Component 场景传入 shadow root\n */\nexport class FloatingAd {\n private opts: ResolvedOptions\n private readonly container: HTMLElement | ShadowRoot\n private el: HTMLElement | null = null\n private closeBtn: HTMLButtonElement | null = null\n /** 本次渲染的广告列表(与 DOM 上的 data-index 一一对应) */\n private rendered: FloatingAdItem[] = []\n\n constructor(\n options: FloatingAdOptions = {},\n container: HTMLElement | ShadowRoot = document.body\n ) {\n this.opts = resolveOptions(options)\n this.container = container\n\n if (this.opts.hideOnMobile && isMobile()) return\n if (this.opts.closePersistence && hasCloseMark()) return\n\n this.render()\n }\n\n /** 是否已挂载到页面 */\n isMounted(): boolean {\n return this.el !== null && this.el.isConnected\n }\n\n /** 打开(已关闭后再次显示,忽略 localStorage 关闭标记) */\n open(): void {\n if (this.isMounted()) return\n this.render()\n }\n\n /** 关闭浮窗(完全移除)。默认不记住状态,刷新后重新出现 */\n close(): void {\n if (this.opts.closePersistence) {\n setCloseMark()\n }\n this.destroy()\n }\n\n /** 从页面移除并解绑 */\n destroy(): void {\n this.closeBtn?.removeEventListener('click', this.handleClose)\n this.closeBtn = null\n this.el?.removeEventListener('click', this.handleItemClick)\n this.el?.removeEventListener('error', this.handleImageError, true)\n this.el?.remove()\n this.el = null\n this.rendered = []\n }\n\n /** 用新配置重渲染(保留原容器) */\n update(options: FloatingAdOptions): void {\n this.destroy()\n this.opts = resolveOptions(options)\n\n if (this.opts.hideOnMobile && isMobile()) return\n if (this.opts.closePersistence && hasCloseMark()) return\n\n this.render()\n }\n\n private render(): void {\n const { ads, position, zIndex, themeOverrides, adLabel, imageFit, sizeOverrides } = this.opts\n\n if (ads.length === 0) return\n\n const root = document.createElement('div')\n root.className = 'fa-widget'\n root.dataset.position = position\n root.style.zIndex = String(zIndex)\n root.style.setProperty('--fa-img-fit', imageFit)\n // 尺寸只在用户显式配置时写 inline,其余交给 CSS(桌面 / 移动端各一套默认值)\n if (sizeOverrides.width !== undefined) {\n root.style.setProperty('--fa-width', `${sizeOverrides.width}px`)\n }\n if (sizeOverrides.offset !== undefined) {\n root.style.setProperty('--fa-offset', `${sizeOverrides.offset}px`)\n }\n if (sizeOverrides.imageSize !== undefined) {\n root.style.setProperty('--fa-img-size', `${sizeOverrides.imageSize}px`)\n }\n\n // 仅把用户显式设置的主题写为 inline(用户优先);\n // 未设置的 key 交给 CSS 变量默认值,以便跟随系统深浅色(prefers-color-scheme)\n const overrides = [\n ['--fa-bg', themeOverrides.bg],\n ['--fa-accent', themeOverrides.accent],\n ['--fa-radius', themeOverrides.radius],\n ['--fa-border', themeOverrides.border]\n ] as const\n for (const [key, value] of overrides) {\n if (value !== undefined) root.style.setProperty(key, value)\n }\n\n // 「广告」标识不单独占行:压在首图左上角\n const label = adLabel ? escapeHtml(String(adLabel)) : ''\n\n root.innerHTML = `\n <button class=\"fa-close\" type=\"button\" aria-label=\"关闭广告\">${CLOSE_SVG}</button>\n ${ads\n .map((ad, index) => this.renderItem(ad, index, index === 0 ? label : ''))\n .join('<div class=\"fa-divider\" role=\"separator\"></div>')}\n `\n\n this.closeBtn = root.querySelector<HTMLButtonElement>('.fa-close')\n this.closeBtn?.addEventListener('click', this.handleClose)\n root.addEventListener('click', this.handleItemClick)\n // 图片加载失败时降级:隐藏空图框,仅保留文字;连文字都没有就整条隐藏\n root.addEventListener('error', this.handleImageError, true)\n\n this.rendered = ads\n this.container.appendChild(root)\n this.el = root\n }\n\n private renderItem(ad: FloatingAdItem, index: number, label: string): string {\n const media = `<span class=\"fa-media\">\n <img class=\"fa-img\" src=\"${escapeAttr(ad.src)}\" alt=\"${escapeAttr(ad.alt ?? ad.title ?? '')}\" loading=\"lazy\" decoding=\"async\" />\n ${ad.badge ? `<span class=\"fa-tag\">${escapeHtml(ad.badge)}</span>` : ''}\n ${label ? `<span class=\"fa-label\">${label}</span>` : ''}\n </span>`\n\n const body =\n ad.title || ad.desc\n ? `<span class=\"fa-body\">\n ${ad.title ? `<span class=\"fa-title\">${escapeHtml(ad.title)}</span>` : ''}\n ${ad.desc ? `<span class=\"fa-desc\">${escapeHtml(ad.desc)}</span>` : ''}\n </span>`\n : ''\n\n const inner = `${media}${body}`\n\n const href = ad.href ? safeHref(ad.href) : null\n if (href) {\n const target = ad.target === '_self' ? '_self' : '_blank'\n const rel =\n target === '_blank'\n ? ' rel=\"noopener noreferrer nofollow sponsored\"'\n : ' rel=\"nofollow sponsored\"'\n return `<a class=\"fa-item\" href=\"${escapeAttr(href)}\" target=\"${target}\"${rel} data-index=\"${index}\">${inner}</a>`\n }\n\n return `<div class=\"fa-item\" data-index=\"${index}\">${inner}</div>`\n }\n\n private readonly handleClose = (): void => {\n this.close()\n }\n\n private readonly handleImageError = (event: Event): void => {\n const img = event.target as HTMLElement | null\n if (!img || img.tagName !== 'IMG' || !img.classList.contains('fa-img')) return\n const media = img.closest('.fa-media')\n if (!media) return\n media.classList.add('fa-media--failed')\n const item = media.closest('.fa-item')\n if (item && !item.querySelector('.fa-body')) item.classList.add('fa-item--failed')\n }\n\n private readonly handleItemClick = (event: Event): void => {\n if (!this.opts.onClick) return\n const target = event.target as Element | null\n const item = target?.closest?.('.fa-item') as HTMLElement | null\n if (!item) return\n const index = Number(item.dataset.index)\n const ad = this.rendered[index]\n if (!ad) return\n this.opts.onClick(ad, event as MouseEvent)\n }\n}\n\nconst CLOSE_SVG =\n '<svg viewBox=\"0 0 24 24\" width=\"14\" height=\"14\" aria-hidden=\"true\" focusable=\"false\"><path d=\"M6 6l12 12M18 6L6 18\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"/></svg>'\n\n/** 过滤危险协议(javascript: / data: / vbscript: 等),返回可用链接或 null */\nfunction safeHref(href: string): string | null {\n const trimmed = href.trim()\n if (!trimmed) return null\n const compact = trimmed.replace(/[\\s\\u0000-\\u001f]/g, '').toLowerCase()\n if (/^(javascript|data|vbscript|file):/.test(compact)) return null\n return trimmed\n}\n\nfunction escapeHtml(value: string): string {\n return value\n .replace(/&/g, '&amp;')\n .replace(/</g, '&lt;')\n .replace(/>/g, '&gt;')\n .replace(/\"/g, '&quot;')\n .replace(/'/g, '&#39;')\n}\n\nfunction escapeAttr(value: string): string {\n return escapeHtml(value)\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/index.ts","../src/FloatingAd.ts"],"sourcesContent":["import { FloatingAd } from './FloatingAd'\n\nexport { FloatingAd }\nexport default FloatingAd\nexport type { FloatingAdOptions, FloatingAdItem, FloatingAdTheme, Position } from './FloatingAd'\n","export type Position =\n | 'right-bottom'\n | 'right-top'\n | 'right-center'\n | 'left-bottom'\n | 'left-top'\n | 'left-center'\n\n/** 单条广告 */\nexport interface FloatingAdItem {\n /** 广告图片地址(必填;没有图片的条目不会渲染) */\n src: string\n /** 标题 */\n title?: string\n /** 副文案 / 卖点 */\n desc?: string\n /** 点击跳转链接(可选,不传则不可点击) */\n href?: string\n /** 跳转方式,默认 `_blank`(仅在传了 href 时生效) */\n target?: '_blank' | '_self'\n /** 图片 alt,缺省使用标题 */\n alt?: string\n /** 图片右上角角标文案,如「限时」 */\n badge?: string\n}\n\nexport interface FloatingAdTheme {\n /** 浮窗背景色 */\n bg?: string\n /** 强调色(标题、角标) */\n accent?: string\n /** 圆角大小 */\n radius?: string\n /** 边框颜色 */\n border?: string\n}\n\nexport interface FloatingAdOptions {\n /** 广告列表(按数组顺序自上而下排列;为空时不渲染浮窗) */\n ads?: FloatingAdItem[]\n /** 浮窗位置,缺省:left-center(桌面与移动端一致,左侧垂直居中) */\n position?: Position\n /** 关闭后是否记住状态(写入 localStorage),默认 false —— false 时刷新页面必然重新出现 */\n closePersistence?: boolean\n /** 移动端(<768px)是否隐藏,默认 false(移动端展示更小一号的浮窗) */\n hideOnMobile?: boolean\n /** 浮窗 z-index,默认 9999 */\n zIndex?: number\n /** 「广告」标识文案(压在首图左上角),默认 false 不显示;传字符串则显示该文案 */\n adLabel?: string | false\n /** 浮窗宽度(px),缺省:桌面 150 / 移动端 140 */\n width?: number\n /** 浮窗距视口边缘距离(px),缺省:桌面 16 / 移动端 12 */\n offset?: number\n /** 图片填充方式,默认 cover */\n imageFit?: 'cover' | 'contain'\n /** 广告图片边长上限(px,正方形)。缺省铺满卡片宽度;设置后小于卡片宽度时居中 */\n imageSize?: number\n /** 主题(映射为 CSS 变量,也可直接覆盖 --fa-* 变量) */\n theme?: FloatingAdTheme\n /** 点击广告回调(不影响默认跳转行为) */\n onClick?: (ad: FloatingAdItem, event: MouseEvent) => void\n}\n\ninterface ResolvedOptions {\n ads: FloatingAdItem[]\n position: Position\n closePersistence: boolean\n hideOnMobile: boolean\n zIndex: number\n adLabel: string | false\n imageFit: 'cover' | 'contain'\n /** 用户显式传入的尺寸子集(未配置时不写 inline,交给 CSS 按桌面/移动端默认值走) */\n sizeOverrides: {\n width?: number\n offset?: number\n imageSize?: number\n }\n theme: Required<FloatingAdTheme>\n /** 用户显式传入的主题子集(未配置的 key 交给 CSS 默认值,可随系统深浅色变化) */\n themeOverrides: FloatingAdTheme\n onClick?: (ad: FloatingAdItem, event: MouseEvent) => void\n}\n\nconst DEFAULT_THEME: Required<FloatingAdTheme> = {\n bg: 'rgba(255, 255, 255, 0.96)',\n accent: '#333',\n radius: '12px',\n border: 'rgba(0, 0, 0, 0.1)'\n}\n\nconst STORAGE_KEY = 'floating-ad:closed'\nconst MOBILE_QUERY = '(max-width: 767px)'\n\nfunction isMobile(): boolean {\n if (typeof window === 'undefined') return false\n return window.matchMedia?.(MOBILE_QUERY)?.matches ?? false\n}\n\nfunction hasCloseMark(): boolean {\n if (typeof localStorage === 'undefined') return false\n try {\n return localStorage.getItem(STORAGE_KEY) === '1'\n } catch {\n return false\n }\n}\n\nfunction setCloseMark(): void {\n if (typeof localStorage === 'undefined') return\n try {\n localStorage.setItem(STORAGE_KEY, '1')\n } catch {\n /* ignore quota / privacy mode */\n }\n}\n\n/** 只有带图片的条目才会渲染(本组件只承载图片广告) */\nfunction isRenderableAd(ad: FloatingAdItem | null | undefined): ad is FloatingAdItem {\n return Boolean(ad && typeof ad === 'object' && ad.src?.trim())\n}\n\nfunction resolveOptions(options: FloatingAdOptions): ResolvedOptions {\n const raw = Array.isArray(options.ads) ? options.ads : []\n return {\n ads: raw.filter(isRenderableAd),\n // 桌面与移动端缺省一致:左侧垂直居中(不挡正文,也不压右下角的返回顶部按钮)\n position: options.position ?? 'left-center',\n closePersistence: options.closePersistence ?? false,\n hideOnMobile: options.hideOnMobile ?? false,\n zIndex: options.zIndex ?? 9999,\n adLabel: options.adLabel === undefined ? false : options.adLabel,\n imageFit: options.imageFit === 'contain' ? 'contain' : 'cover',\n sizeOverrides: {\n width: Number.isFinite(options.width) ? Math.max(80, Number(options.width)) : undefined,\n offset: Number.isFinite(options.offset) ? Math.max(0, Number(options.offset)) : undefined,\n imageSize: Number.isFinite(options.imageSize)\n ? Math.max(40, Number(options.imageSize))\n : undefined\n },\n theme: { ...DEFAULT_THEME, ...(options.theme ?? {}) },\n themeOverrides: options.theme ?? {},\n onClick: options.onClick\n }\n}\n\n/**\n * A minimal floating ad slot that pins image / text ads to the left (or right) edge.\n * Zero dependencies. Framework-agnostic. Styleable via --fa-* CSS variables.\n *\n * 没有广告数据时不渲染任何节点(不会留下空白卡片)。\n *\n * @param container 渲染容器,默认 document.body;Web Component 场景传入 shadow root\n */\nexport class FloatingAd {\n private opts: ResolvedOptions\n private readonly container: HTMLElement | ShadowRoot\n private el: HTMLElement | null = null\n private closeBtn: HTMLButtonElement | null = null\n /** 本次渲染的广告列表(与 DOM 上的 data-index 一一对应) */\n private rendered: FloatingAdItem[] = []\n\n constructor(\n options: FloatingAdOptions = {},\n container: HTMLElement | ShadowRoot = document.body\n ) {\n this.opts = resolveOptions(options)\n this.container = container\n\n if (this.opts.hideOnMobile && isMobile()) return\n if (this.opts.closePersistence && hasCloseMark()) return\n\n this.render()\n }\n\n /** 是否已挂载到页面 */\n isMounted(): boolean {\n return this.el !== null && this.el.isConnected\n }\n\n /** 打开(已关闭后再次显示,忽略 localStorage 关闭标记) */\n open(): void {\n if (this.isMounted()) return\n this.render()\n }\n\n /** 关闭浮窗(完全移除)。默认不记住状态,刷新后重新出现 */\n close(): void {\n if (this.opts.closePersistence) {\n setCloseMark()\n }\n this.destroy()\n }\n\n /** 从页面移除并解绑 */\n destroy(): void {\n this.closeBtn?.removeEventListener('click', this.handleClose)\n this.closeBtn = null\n this.el?.removeEventListener('click', this.handleItemClick)\n this.el?.removeEventListener('error', this.handleImageError, true)\n this.el?.remove()\n this.el = null\n this.rendered = []\n }\n\n /** 用新配置重渲染(保留原容器) */\n update(options: FloatingAdOptions): void {\n this.destroy()\n this.opts = resolveOptions(options)\n\n if (this.opts.hideOnMobile && isMobile()) return\n if (this.opts.closePersistence && hasCloseMark()) return\n\n this.render()\n }\n\n private render(): void {\n const { ads, position, zIndex, themeOverrides, adLabel, imageFit, sizeOverrides } = this.opts\n\n if (ads.length === 0) return\n\n const root = document.createElement('div')\n root.className = 'fa-widget'\n root.dataset.position = position\n root.style.zIndex = String(zIndex)\n root.style.setProperty('--fa-img-fit', imageFit)\n // 尺寸只在用户显式配置时写 inline,其余交给 CSS(桌面 / 移动端各一套默认值)\n if (sizeOverrides.width !== undefined) {\n root.style.setProperty('--fa-width', `${sizeOverrides.width}px`)\n }\n if (sizeOverrides.offset !== undefined) {\n root.style.setProperty('--fa-offset', `${sizeOverrides.offset}px`)\n }\n if (sizeOverrides.imageSize !== undefined) {\n root.style.setProperty('--fa-img-size', `${sizeOverrides.imageSize}px`)\n }\n\n // 仅把用户显式设置的主题写为 inline(用户优先);\n // 未设置的 key 交给 CSS 变量默认值,以便跟随系统深浅色(prefers-color-scheme)\n const overrides = [\n ['--fa-bg', themeOverrides.bg],\n ['--fa-accent', themeOverrides.accent],\n ['--fa-radius', themeOverrides.radius],\n ['--fa-border', themeOverrides.border]\n ] as const\n for (const [key, value] of overrides) {\n if (value !== undefined) root.style.setProperty(key, value)\n }\n\n // 「广告」标识不单独占行:压在首图左上角\n const label = adLabel ? escapeHtml(String(adLabel)) : ''\n\n root.innerHTML = `\n <button class=\"fa-close\" type=\"button\" aria-label=\"关闭广告\">${CLOSE_SVG}</button>\n ${ads\n .map((ad, index) => this.renderItem(ad, index, index === 0 ? label : ''))\n .join('<div class=\"fa-divider\" role=\"separator\"></div>')}\n `\n\n this.closeBtn = root.querySelector<HTMLButtonElement>('.fa-close')\n this.closeBtn?.addEventListener('click', this.handleClose)\n root.addEventListener('click', this.handleItemClick)\n // 图片加载失败时降级:隐藏空图框,仅保留文字;连文字都没有就整条隐藏\n root.addEventListener('error', this.handleImageError, true)\n\n this.rendered = ads\n this.container.appendChild(root)\n this.el = root\n }\n\n private renderItem(ad: FloatingAdItem, index: number, label: string): string {\n const media = `<span class=\"fa-media\">\n <img class=\"fa-img\" src=\"${escapeAttr(ad.src)}\" alt=\"${escapeAttr(ad.alt ?? ad.title ?? '')}\" loading=\"lazy\" decoding=\"async\" />\n ${ad.badge ? `<span class=\"fa-tag\">${escapeHtml(ad.badge)}</span>` : ''}\n ${label ? `<span class=\"fa-label\">${label}</span>` : ''}\n </span>`\n\n const body =\n ad.title || ad.desc\n ? `<span class=\"fa-body\">\n ${ad.title ? `<span class=\"fa-title\">${escapeHtml(ad.title)}</span>` : ''}\n ${ad.desc ? `<span class=\"fa-desc\">${escapeHtml(ad.desc)}</span>` : ''}\n </span>`\n : ''\n\n const inner = `${media}${body}`\n\n const href = ad.href ? safeHref(ad.href) : null\n if (href) {\n const target = ad.target === '_self' ? '_self' : '_blank'\n const rel =\n target === '_blank'\n ? ' rel=\"noopener noreferrer nofollow sponsored\"'\n : ' rel=\"nofollow sponsored\"'\n return `<a class=\"fa-item\" href=\"${escapeAttr(href)}\" target=\"${target}\"${rel} data-index=\"${index}\">${inner}</a>`\n }\n\n return `<div class=\"fa-item\" data-index=\"${index}\">${inner}</div>`\n }\n\n private readonly handleClose = (): void => {\n this.close()\n }\n\n private readonly handleImageError = (event: Event): void => {\n const img = event.target as HTMLElement | null\n if (!img || img.tagName !== 'IMG' || !img.classList.contains('fa-img')) return\n const media = img.closest('.fa-media')\n if (!media) return\n media.classList.add('fa-media--failed')\n const item = media.closest('.fa-item')\n if (item && !item.querySelector('.fa-body')) item.classList.add('fa-item--failed')\n }\n\n private readonly handleItemClick = (event: Event): void => {\n if (!this.opts.onClick) return\n const target = event.target as Element | null\n const item = target?.closest?.('.fa-item') as HTMLElement | null\n if (!item) return\n const index = Number(item.dataset.index)\n const ad = this.rendered[index]\n if (!ad) return\n this.opts.onClick(ad, event as MouseEvent)\n }\n}\n\nconst CLOSE_SVG =\n '<svg viewBox=\"0 0 24 24\" width=\"14\" height=\"14\" aria-hidden=\"true\" focusable=\"false\"><path d=\"M6 6l12 12M18 6L6 18\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"/></svg>'\n\n/** 过滤危险协议(javascript: / data: / vbscript: 等),返回可用链接或 null */\nfunction safeHref(href: string): string | null {\n const trimmed = href.trim()\n if (!trimmed) return null\n const compact = trimmed.replace(/[\\s\\u0000-\\u001f]/g, '').toLowerCase()\n if (/^(javascript|data|vbscript|file):/.test(compact)) return null\n return trimmed\n}\n\nfunction escapeHtml(value: string): string {\n return value\n .replace(/&/g, '&amp;')\n .replace(/</g, '&lt;')\n .replace(/>/g, '&gt;')\n .replace(/\"/g, '&quot;')\n .replace(/'/g, '&#39;')\n}\n\nfunction escapeAttr(value: string): string {\n return escapeHtml(value)\n}\n"],"mappings":"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","names":[]}
package/dist/index.umd.js CHANGED
@@ -63,8 +63,8 @@ var FloatingAd = (() => {
63
63
  const raw = Array.isArray(options.ads) ? options.ads : [];
64
64
  return {
65
65
  ads: raw.filter(isRenderableAd),
66
- // 移动端缺省落在左下角(不挡右侧阅读区/返回顶部按钮)
67
- position: (_a = options.position) != null ? _a : isMobile() ? "left-bottom" : "left-center",
66
+ // 桌面与移动端缺省一致:左侧垂直居中(不挡正文,也不压右下角的返回顶部按钮)
67
+ position: (_a = options.position) != null ? _a : "left-center",
68
68
  closePersistence: (_b = options.closePersistence) != null ? _b : false,
69
69
  hideOnMobile: (_c = options.hideOnMobile) != null ? _c : false,
70
70
  zIndex: (_d = options.zIndex) != null ? _d : 9999,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wu529778790/floating-ad",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
4
4
  "description": "A minimal, zero-dependency floating ad slot widget for any website: image or text ads on the left/right edge, closable, themeable via CSS variables.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.umd.cjs",