@familyboat/mini-framework 0.1.0

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 ADDED
@@ -0,0 +1,73 @@
1
+ # mini-framework
2
+
3
+ A lightweight DOM framework with route, modal, animation, interaction, and toast utilities.
4
+
5
+ ## Features
6
+
7
+ - Route-based navigation with lifecycle hooks
8
+ - Modal lifecycle management
9
+ - CSS animation helpers
10
+ - Interaction control via `inert`
11
+ - Toast notifications
12
+ - HTML template files with variable injection via `renderTemplate()`
13
+
14
+ ## Installation
15
+
16
+ For local development in this workspace:
17
+
18
+ ```bash
19
+ npm install
20
+ ```
21
+
22
+ This package depends on Vite as a peer dependency, so the consuming project should install Vite in its own dependencies.
23
+
24
+ ## Publish to npm
25
+
26
+ Before publishing, make sure you are logged in to npm:
27
+
28
+ ```bash
29
+ npm login
30
+ ```
31
+
32
+ Then publish with:
33
+
34
+ ```bash
35
+ npm publish --access public
36
+ ```
37
+
38
+ You can also run a dry run first:
39
+
40
+ ```bash
41
+ npm run publish:check
42
+ ```
43
+
44
+ The library source is exposed through the exported entrypoint in `lib/index.ts`.
45
+
46
+ ## Usage
47
+
48
+ ```ts
49
+ import {
50
+ Toast,
51
+ Router,
52
+ Modal,
53
+ fadeIn,
54
+ renderTemplate,
55
+ } from "@familyboat/mini-framework";
56
+ ```
57
+
58
+ ## Package structure
59
+
60
+ ```text
61
+ lib/
62
+ animate/
63
+ interaction/
64
+ modal/
65
+ router/
66
+ toast/
67
+ index.ts
68
+ README.md
69
+ ```
70
+
71
+ ## Documentation
72
+
73
+ See [lib/README.md](lib/README.md) for the detailed usage guide, lifecycle rules, CSS naming conventions, and component directory structure.
package/lib/README.md ADDED
@@ -0,0 +1,542 @@
1
+ # Mini Framework Lib
2
+
3
+ `lib` 提供四组基础能力:路由、Modal、动画和交互控制。推荐从统一入口导入:
4
+
5
+ ```ts
6
+ import {
7
+ Modal,
8
+ Router,
9
+ fadeIn,
10
+ enterElement,
11
+ disableInteraction,
12
+ } from "lib";
13
+ ```
14
+
15
+ ## 模块职责
16
+
17
+ - `router`:创建和切换页面路由。
18
+ - `modal`:创建、显示、隐藏和销毁 Modal。
19
+ - `animate`:播放 CSS Animation,并处理完成、取消和同元素动画竞争。
20
+ - `interaction`:控制元素是否接受用户交互。
21
+ - `toast`:轻量级通知组件,用于展示短时提示消息。
22
+
23
+ 动画和交互是两个独立概念:动画模块不会移除用户事件监听器;交互模块只负责交互状态。
24
+
25
+ ## CSS 类名命名规则
26
+
27
+ 整个项目要保持一致的前提是:不同层次的类名分别遵守不同的命名约定,而不是混用。
28
+
29
+ ### 1) 组件根类
30
+
31
+ 组件根类使用单独的基础名,并保留实例级别的命名:
32
+
33
+ - `page`:页面容器根类
34
+ - `page-${name}`:具体页面的实例根类
35
+ - `modal`:弹层容器根类
36
+ - `modal-${name}`:具体 Modal 的实例根类
37
+ - `toast-container`:Toast 容器根类
38
+ - `toast`:单个 Toast 根类
39
+
40
+ 这样可以一眼看出:
41
+
42
+ - `page` / `modal` / `toast` 是公共基础结构
43
+ - `page-${name}` / `modal-${name}` 是实例特有的标识
44
+
45
+ ### 2) 组件内部元素使用 BEM
46
+
47
+ 组件内部的元素统一使用 BEM 风格:
48
+
49
+ - `__` 表示元素,例如 `toast__message`、`toast__close`
50
+ - `--` 表示修饰状态,例如 `toast--success`、`toast--error`
51
+ - `toast-container--top` / `toast-container--bottom` 表示容器位置修饰
52
+
53
+ 因此,整个项目中,组件内部的“结构类名”应统一使用:
54
+
55
+ - `block__element`
56
+ - `block--modifier`
57
+
58
+ 例如:
59
+
60
+ ```css
61
+ .modal__dialog {}
62
+ .modal__mask {}
63
+ .modal__content {}
64
+ .toast__message {}
65
+ .toast__close {}
66
+ .toast--success {}
67
+ ```
68
+
69
+ ### 3) CSS 变量统一使用 `--z-index-*` 命名
70
+
71
+ 全局层级变量统一使用 `--z-index-*` 的命名形式,当前项目中的变量如下:
72
+
73
+ ```css
74
+ :root {
75
+ --z-index-page: 1;
76
+ --z-index-modal: 10;
77
+ --z-index-toast: 100;
78
+ }
79
+ ```
80
+
81
+ 这里的命名规则是:
82
+
83
+ - `--z-index-page`:页面层级
84
+ - `--z-index-modal`:弹层层级
85
+ - `--z-index-toast`:Toast 层级
86
+
87
+ 后续新增的层级变量也应遵循同一规则:`--z-index-<component-name>`。
88
+
89
+ 这类变量的作用是统一声明层级顺序,避免各处散落的 magic number。
90
+
91
+ ### 4) 动画类名单独使用 kebab-case
92
+
93
+ 动画类是 CSS Animation 的入口,不属于组件 BEM 结构,因此统一使用简单的 kebab-case:
94
+
95
+ - `fade-in`
96
+ - `fade-out`
97
+ - `left-slide-in`
98
+ - `right-slide-in`
99
+ - `left-slide-out`
100
+ - `right-slide-out`
101
+
102
+ 这类类名的职责是“告诉动画模块应该播放哪一种动画”,因此它们不应再混用 BEM 的 `__` / `--` 语法。
103
+
104
+ ### 4) 代码中的私有字段
105
+
106
+ TypeScript / JavaScript 中的私有字段使用 `_` 前缀:
107
+
108
+ - `_root`
109
+ - `_container`
110
+ - `_timer`
111
+ - `_operationId`
112
+
113
+ 这部分是内部状态,不属于 CSS 类名,不参与 DOM 结构命名。
114
+
115
+ ### 5) 项目里当前的统一规则
116
+
117
+ 项目里推荐的统一规则总结如下:
118
+
119
+ ```text
120
+ 根类:page / modal / toast / toast-container
121
+ 实例类:page-${name} / modal-${name}
122
+ 元素:block__element
123
+ 修饰:block--modifier
124
+ 动画:fade-in / left-slide-in / ...
125
+ 私有字段:_fieldName
126
+ ```
127
+
128
+ 在实际开发中,新的 CSS 类名应优先遵循这个规则;如果发现旧类名存在混用,则应尽快改为统一的命名形式。
129
+
130
+ ## 组件目录结构约定
131
+
132
+ 为了让项目结构更稳定、可扩展,也更接近 Angular 的组件组织方式,后续新增的页面和弹窗建议统一采用“目录即组件”的组织形式。
133
+
134
+ 推荐结构:
135
+
136
+ ```text
137
+ src/
138
+ pages/
139
+ landing/
140
+ index.ts
141
+ index.html
142
+ index.css
143
+ loading/
144
+ index.ts
145
+ index.html
146
+ index.css
147
+ modal/
148
+ leaderboard/
149
+ index.ts
150
+ index.css
151
+ index.html
152
+ ```
153
+
154
+ ### 规则
155
+
156
+ - 目录名就是组件名,例如 `landing`、`loading`、`leaderboard`
157
+ - 组件目录内统一使用以下三类文件:
158
+ - `index.ts`:组件逻辑
159
+ - `index.html`:组件模板
160
+ - `index.css`:组件样式
161
+ - `index.ts` 应优先使用本地模板和样式:
162
+
163
+ ```ts
164
+ import landingTemplate from "./index.html?raw";
165
+ import "./index.css";
166
+ ```
167
+
168
+ - 对外引用时,优先通过目录入口引用,而不是直接指向具体文件名,例如:
169
+
170
+ ```ts
171
+ import { Landing } from "./pages/landing";
172
+ import { Leaderboard } from "./modal/leaderboard";
173
+ ```
174
+
175
+ 这样可以保证组件目录具有明确边界,也方便后续扩展更多文件,例如 `index.spec.ts`、`types.ts`、`assets/` 等。
176
+
177
+ ## 模板渲染约定
178
+
179
+ 如果你希望模板内容像 HTML 文件一样有更好的编辑器支持、标签补全和格式化体验,建议使用真实的 HTML 模板文件来编写结构,再在 TS 中通过 `renderTemplate()` 注入变量。
180
+
181
+ ### 推荐写法
182
+
183
+ ```ts
184
+ import { renderTemplate, Route } from "lib";
185
+ import settingsTemplate from "./index.html?raw";
186
+
187
+ export class Settings extends Route {
188
+ protected render(): void {
189
+ this.root.innerHTML = renderTemplate(settingsTemplate, {
190
+ title: "Settings",
191
+ });
192
+ }
193
+ }
194
+ ```
195
+
196
+ 对应的模板文件:
197
+
198
+ ```html
199
+ <!-- src/pages/settings/index.html -->
200
+ <div class="page">
201
+ <h1>{{title}}</h1>
202
+ </div>
203
+ ```
204
+
205
+ ### 变量插入规则
206
+
207
+ `renderTemplate()` 会把模板中的 `{{name}}` 替换成传入对象里的值:
208
+
209
+ ```ts
210
+ renderTemplate(template, {
211
+ title: "Landing",
212
+ count: 3,
213
+ enabled: true,
214
+ });
215
+ ```
216
+
217
+ 当前实现中会把 `null` / `undefined` 视为空字符串,其他值会转换成字符串。
218
+
219
+ ### 什么时候使用哪种方式
220
+
221
+ - 复杂结构、需要编辑器 HTML 提示、需要更好的格式化体验:优先使用 `.html` 模板文件
222
+ - 纯粹的简单字符串模板、临时内容:也可以继续用 `html\`...\``
223
+
224
+ `html\`...\``` 仍然保留作为运行时的通用模板能力,但从工程约定上看,结构较复杂的页面更推荐使用 `.html` 模板文件。
225
+
226
+ ## Toast
227
+
228
+ Toast 是一个独立的轻量通知组件,不继承 `Route` 或 `Modal`,适合用于显示短时提示。
229
+
230
+ ### 公开 API
231
+
232
+ ```ts
233
+ import { Toast } from "lib";
234
+
235
+ Toast.show({
236
+ message: "保存成功",
237
+ kind: "success",
238
+ duration: 3000,
239
+ position: "top",
240
+ });
241
+ ```
242
+
243
+ ### 可用参数
244
+
245
+ ```ts
246
+ export type ToastProps = {
247
+ message: string;
248
+ duration?: number;
249
+ position?: "top" | "bottom";
250
+ kind?: "info" | "success" | "error";
251
+ };
252
+ ```
253
+
254
+ ### 常用用法
255
+
256
+ ```ts
257
+ Toast.show({ message: "Hello" });
258
+ Toast.show({ message: "上传失败", kind: "error" });
259
+ Toast.show({ message: "已保存", kind: "success", position: "bottom" });
260
+ ```
261
+
262
+ ### 行为约定
263
+
264
+ - 默认 `duration` 为 `3000ms`
265
+ - 默认 `position` 为 `top`
266
+ - 默认 `kind` 为 `info`
267
+ - `Toast.clear()` 可清空当前所有可见 Toast
268
+ - `Toast` 会自动管理容器,避免多次重复创建同位置节点
269
+
270
+ ### 组件目录结构
271
+
272
+ Toast 也遵循同样的目录组织方式:
273
+
274
+ ```text
275
+ lib/
276
+ toast/
277
+ index.ts
278
+ index.css
279
+ ```
280
+
281
+ ## Router
282
+
283
+ ### 创建 Route
284
+
285
+ Route 不能直接通过 `new` 创建。具体页面应使用继承自基类的 `static create()` 工厂:
286
+
287
+ ```ts
288
+ import { Route } from "lib";
289
+ import type { RouteProps } from "lib";
290
+
291
+ export class Settings extends Route {
292
+ protected render(): void {
293
+ this.root.innerHTML = renderTemplate(settingsTemplate, {
294
+ title: "Settings",
295
+ });
296
+ }
297
+
298
+ protected onEnter(operationId: number): void {
299
+ // 当该路由变为当前路由时触发
300
+ // operationId 可以用来判定异步任务是否已失效
301
+ }
302
+
303
+ protected onLeave(operationId: number): void {
304
+ // 当该路由被切走时触发
305
+ // 可在这里停止定时器、清理状态和播放离场动画
306
+ }
307
+ }
308
+ ```
309
+
310
+ `create()` 会在基类中完成 `new`、`initialize()` 和缓存注册。这样可以保证实例在进入业务代码前已经渲染并注册完成。
311
+
312
+ ### 注册和导航
313
+
314
+ ```ts
315
+ Settings.create({ name: "settings" });
316
+ Router.navigate("settings");
317
+ ```
318
+
319
+ `Router.navigate(name)` 的行为:
320
+
321
+ - 未注册的名称会抛出错误。
322
+ - 导航到当前路由会被忽略。
323
+ - 切换路由时会先调用旧路由的 `hide()`,再调用新路由的 `show()`。
324
+ - 每次有效导航都会产生新的操作编号。
325
+
326
+ Route 和 Modal 都支持 `stale-operation` 保护:每次 `show()` / `hide()` 都会递增一个操作编号,后续异步回调在执行前会校验编号是否仍然是当前最新值。
327
+
328
+ 页面中的延迟任务可以保存当前 `operationId`,并在任务执行时校验:
329
+
330
+ ```ts
331
+ const operationId = this.beginOperation();
332
+
333
+ setTimeout(() => {
334
+ if (!this.isCurrentOperation(operationId)) {
335
+ return; // 当前任务已经过期
336
+ }
337
+
338
+ Router.navigate("settings");
339
+ }, 1000);
340
+ ```
341
+
342
+ 在 Route 中,`show()` / `hide()` 会自动触发 `onEnter(operationId)` / `onLeave(operationId)`,因此子类可以直接接收并校验当前有效性;在 Modal 中,`show()` / `hide()` 也会自动触发 `onShow(operationId)` / `onHide(operationId)`,并在需要时通过 `isCurrentOperation(operationId)` 判断异步回调是否仍然有效。
343
+
344
+ ### 公开生命周期约定
345
+
346
+ 无论是 Route 还是 Modal,公开 API 都遵循同一套约定:
347
+
348
+ - 具体类型不应直接暴露 `new` 构造入口,统一依赖基类的 `create()` 工厂。
349
+ - `create()` 会做两件事:先完成 `new`,再立即调用受保护的 `initialize()`。
350
+ - `initialize()` 负责一次性渲染与缓存注册;失败时会回滚实例与 DOM。
351
+ - `show()` / `hide()` 会先调用 `beginOperation()`,生成新的 `operationId`,然后再进入对应的生命周期钩子。
352
+ - 子类在 `onEnter()` / `onLeave()` / `onShow()` / `onHide()` 中实现业务行为时,应在异步回调里校验 `operationId` 是否仍然有效。
353
+ - `dispose()` 只负责清理子类自身创建的资源,不能替代 `render()` 与 `initialize()` 的初始化职责。
354
+
355
+ 这样可以保证:调用方拿到的是已经初始化好的实例,而不是半成品;同时,也可以避免旧异步动作覆盖最新状态。
356
+
357
+ ## Modal
358
+
359
+ ### 创建 Modal
360
+
361
+ Modal 也必须通过继承自基类的 `static create()` 工厂创建:
362
+
363
+ ```ts
364
+ import { Modal } from "lib";
365
+ import type { ModalProps } from "lib";
366
+
367
+ class HelpModal extends Modal {
368
+ protected render(): void {
369
+ this.root.innerHTML = renderTemplate(helpModalTemplate, {
370
+ title: "Help",
371
+ });
372
+ }
373
+
374
+ protected onShow(operationId: number): void {
375
+ // 播放进入动画
376
+ // operationId 可以在需要时用于异步任务有效性判断
377
+ }
378
+
379
+ protected onHide(operationId: number): void {
380
+ // 异步隐藏完成后检查操作是否仍然有效
381
+ if (this.isCurrentOperation(operationId)) {
382
+ // 隐藏 root
383
+ }
384
+ }
385
+
386
+ protected dispose(): void {
387
+ // 移除子类自己注册的事件、定时器和订阅
388
+ }
389
+ }
390
+
391
+ HelpModal.create({ name: "help" });
392
+ ```
393
+
394
+ Modal 的生命周期:
395
+
396
+ ```text
397
+ 具体工厂
398
+ -> private constructor
399
+ -> super
400
+ -> initialize
401
+ -> render
402
+ -> 注册到 Modal 缓存
403
+ -> 返回已初始化实例
404
+ ```
405
+
406
+ 不要直接调用 `render()`,也不要直接 `new` 具体 Modal。`render()` 是受保护的,并且每个实例只能通过 `initialize()` 成功执行一次。
407
+
408
+ ### 显示、隐藏和销毁
409
+
410
+ ```ts
411
+ Modal.show("help");
412
+ Modal.hide("help");
413
+ Modal.destroy("help");
414
+ ```
415
+
416
+ 也可以保存工厂返回的实例:
417
+
418
+ ```ts
419
+ const help = HelpModal.create({ name: "help" });
420
+ help.destroy();
421
+ ```
422
+
423
+ `destroy()` 会:
424
+
425
+ - 使未完成的 Modal 操作失效。
426
+ - 调用子类的 `dispose()`。
427
+ - 从 Modal 注册表中移除实例。
428
+ - 从文档中移除 Modal 根节点。
429
+
430
+ 子类应该在 `dispose()` 中清理自己创建的资源,例如绑定在 `window`、`document` 或其他外部对象上的监听器,以及定时器和订阅。
431
+
432
+ 基类公开的 `show()` 和 `hide()` 会自动检查销毁状态并生成操作编号。子类只需要实现受保护的 `onShow(operationId)` 和 `onHide(operationId)`,不需要手动调用 `assertUsable()` 或 `beginOperation()`。
433
+
434
+ > 统一设计说明:Route 和 Modal 都采用同一套 `stale-operation` 保护模型。不同之处在于 Route 的异步入参通常在 `onEnter/onLeave` 中接收,而 Modal 的异步入参通常在 `onHide` 中接收;但两者的核心思想都是相同的:让旧回调在新的操作发生后自动失效,防止过期状态覆盖当前状态。
435
+
436
+ ## Animate
437
+
438
+ ### 内置动画
439
+
440
+ ```ts
441
+ fadeIn(element);
442
+ fadeOut(element);
443
+ leftSlideIn(element);
444
+ rightSlideIn(element);
445
+ leftSlideOut(element);
446
+ rightSlideOut(element);
447
+ ```
448
+
449
+ 这些函数返回 `Promise<boolean>`:
450
+
451
+ - `true`:动画正常结束。
452
+ - `false`:动画被取消。
453
+
454
+ 可以传入 CSS 时间配置:
455
+
456
+ ```ts
457
+ fadeIn(element, {
458
+ duration: "300ms",
459
+ delay: "100ms",
460
+ });
461
+ ```
462
+
463
+ 配置通过 `--duration` 和 `--delay` 传递给 CSS Animation,动画结束后会恢复调用前的值。
464
+
465
+ ### 自定义动画
466
+
467
+ 不需要修改动画模块,只要在自己的 CSS 中让 class 名与 `@keyframes` 名相同:
468
+
469
+ ```css
470
+ .bounce-in {
471
+ --duration: 500ms;
472
+ --delay: 0s;
473
+ animation: bounce-in var(--duration) ease var(--delay);
474
+ }
475
+
476
+ @keyframes bounce-in {
477
+ from {
478
+ opacity: 0;
479
+ transform: scale(0.8);
480
+ }
481
+
482
+ to {
483
+ opacity: 1;
484
+ transform: scale(1);
485
+ }
486
+ }
487
+ ```
488
+
489
+ 然后调用:
490
+
491
+ ```ts
492
+ enterElement(element, "bounce-in");
493
+ ```
494
+
495
+ 离场动画使用:
496
+
497
+ ```ts
498
+ leaveElement(element, "bounce-out");
499
+ ```
500
+
501
+ `leaveElement()` 在动画正常结束后会隐藏元素;如果动画被取消,则不会执行隐藏回调。
502
+
503
+ ### 动画并发规则
504
+
505
+ 同一个元素同时只能保留一个活动动画:
506
+
507
+ ```ts
508
+ const first = fadeIn(element);
509
+ const second = fadeOut(element);
510
+ ```
511
+
512
+ 后启动的动画会取消前一个动画。前一个 Promise 会解析为 `false`,新动画继续执行。
513
+
514
+ 同名动画不会被自动去重;JavaScript 重复调用仍会启动新的动画。动画模块处理的是动画竞争,不是命令式调用去重。
515
+
516
+ ## Interaction
517
+
518
+ ### 独立控制交互
519
+
520
+ ```ts
521
+ import { disableInteraction } from "lib";
522
+
523
+ const restore = disableInteraction(element);
524
+
525
+ // 此时 element 及其后代不会接受用户交互
526
+ restore();
527
+ ```
528
+
529
+ `disableInteraction()` 使用元素的 `inert` 状态,并返回恢复函数。调用时会保存原有状态,因此恢复时不会强行覆盖调用前的设置。
530
+
531
+ 动画模块会在动画开始时自动禁用动画根元素的交互,并在动画正常结束或取消时恢复。这个行为只改变用户交互状态,不会移除事件监听器,也不会阻止 JavaScript 直接调用动画函数。
532
+
533
+ ## 推荐约定
534
+
535
+ ```text
536
+ Route / Modal:负责生命周期和业务状态
537
+ Animate:负责 CSS Animation 的播放、取消和完成结果
538
+ Interaction:负责 inert、焦点和用户交互状态
539
+ 用户代码:负责事件、定时器、请求和订阅的业务清理
540
+ ```
541
+
542
+ 创建 Route 或 Modal 时始终使用具体类的 `create()` 工厂;实现异步 `show()` 或 `hide()` 时使用操作编号,避免过期回调覆盖更新后的状态。
@@ -0,0 +1,95 @@
1
+ .fade-in {
2
+ --duration: 1.5s;
3
+ --delay: 0s;
4
+ animation: fade-in var(--duration) ease-in var(--delay);
5
+ }
6
+
7
+ @keyframes fade-in {
8
+ from {
9
+ opacity: 0;
10
+ }
11
+
12
+ to {
13
+ opacity: 1;
14
+ }
15
+ }
16
+
17
+ .fade-out {
18
+ --duration: 1.5s;
19
+ --delay: 0s;
20
+ animation: fade-out var(--duration) ease-out var(--delay);
21
+ }
22
+
23
+ @keyframes fade-out {
24
+ from {
25
+ opacity: 1;
26
+ }
27
+
28
+ to {
29
+ opacity: 0;
30
+ }
31
+ }
32
+
33
+ .left-slide-in {
34
+ --duration: 1.5s;
35
+ --delay: 0s;
36
+ animation: left-slide-in var(--duration) ease-in var(--delay);
37
+ }
38
+
39
+ @keyframes left-slide-in {
40
+ from {
41
+ transform: translateX(-100%);
42
+ }
43
+
44
+ to {
45
+ transform: translateX(0);
46
+ }
47
+ }
48
+
49
+ .right-slide-in {
50
+ --duration: 1.5s;
51
+ --delay: 0s;
52
+ animation: right-slide-in var(--duration) ease-in var(--delay);
53
+ }
54
+
55
+ @keyframes right-slide-in {
56
+ from {
57
+ transform: translateX(100%);
58
+ }
59
+
60
+ to {
61
+ transform: translateX(0);
62
+ }
63
+ }
64
+
65
+ .left-slide-out {
66
+ --duration: 1.5s;
67
+ --delay: 0s;
68
+ animation: left-slide-out var(--duration) ease-out var(--delay);
69
+ }
70
+
71
+ @keyframes left-slide-out {
72
+ from {
73
+ transform: translateX(0);
74
+ }
75
+
76
+ to {
77
+ transform: translateX(-100%);
78
+ }
79
+ }
80
+
81
+ .right-slide-out {
82
+ --duration: 1.5s;
83
+ --delay: 0s;
84
+ animation: right-slide-out var(--duration) ease-out var(--delay);
85
+ }
86
+
87
+ @keyframes right-slide-out {
88
+ from {
89
+ transform: translateX(0);
90
+ }
91
+
92
+ to {
93
+ transform: translateX(100%);
94
+ }
95
+ }