@done-coding/admin-core 0.23.1-alpha.0 → 0.24.1-alpha.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/es/bridge/badge.mjs +27 -0
- package/es/bridge/index.mjs +4 -0
- package/es/components/app-layout/AppHeader.vue.mjs +1 -1
- package/es/components/app-layout/AppHeader.vue2.mjs +13 -3
- package/es/components/app-layout/AppLayout.vue.mjs +1 -1
- package/es/components/app-layout/AppLayout.vue2.mjs +6 -2
- package/es/components/display/ActionBtn.vue.mjs +21 -3
- package/es/components/display/BadgeMark.vue.mjs +7 -0
- package/es/components/display/BadgeMark.vue2.mjs +31 -0
- package/es/components/display/TabsHeader.vue.mjs +1 -1
- package/es/components/display/TabsHeader.vue2.mjs +16 -7
- package/es/components/display/TabsTile.vue.mjs +1 -1
- package/es/components/display/TabsTile.vue2.mjs +27 -7
- package/es/components/display/use-badge.mjs +62 -0
- package/es/components/menu/MenuItemSub.vue.mjs +22 -3
- package/es/components/menu/MenuTree.vue.mjs +34 -8
- package/es/index.mjs +62 -60
- package/es/store/app.mjs +16 -4
- package/es/style.css +61 -46
- package/package.json +2 -2
- package/src/bridge/README.md +1 -1
- package/src/bridge/docs/README.md +98 -2
- package/src/components/app-layout/README.md +1 -1
- package/src/components/app-layout/docs/README-AppHeader.md +8 -0
- package/src/components/app-layout/docs/README-AppLayout.md +24 -0
- package/src/components/display/README.md +7 -4
- package/src/components/display/docs/README-ActionBtn.md +1 -0
- package/src/components/display/docs/README-BadgeMark.md +125 -0
- package/src/components/display/docs/README-TabsMain.md +20 -1
- package/src/components/menu/README.md +8 -0
- package/src/components/menu/docs/README-MenuItemSub.md +4 -0
- package/src/components/menu/docs/README-MenuTree.md +10 -0
- package/types/bridge/badge.d.ts +112 -0
- package/types/bridge/index.d.ts +18 -0
- package/types/components/display/BadgeMark.vue.d.ts +57 -0
- package/types/components/display/index.d.ts +7 -0
- package/types/components/display/types.d.ts +18 -0
- package/types/components/display/use-badge.d.ts +19 -0
- package/types/injectInfo.json.d.ts +1 -1
- package/types/store/app.d.ts +22 -1
- package/types/types/route.d.ts +20 -0
package/es/index.mjs
CHANGED
|
@@ -43,42 +43,43 @@ import { default as default40 } from "./components/app-layout/AppSidebar.vue.mjs
|
|
|
43
43
|
import { default as default41 } from "./components/app-layout/AppTheme.vue.mjs";
|
|
44
44
|
import { default as default42 } from "./components/misc/AutoRefresh.vue.mjs";
|
|
45
45
|
import { default as default43 } from "./components/misc/AutoRefreshGroup.vue.mjs";
|
|
46
|
+
import { default as default44 } from "./components/display/BadgeMark.vue.mjs";
|
|
46
47
|
import { CORE_VIEWPORT_DECOR_ZINDEX_ABOVE, CORE_VIEWPORT_DECOR_ZINDEX_BELOW } from "./components/app-layout/viewport.mjs";
|
|
47
|
-
import { default as
|
|
48
|
+
import { default as default45, default as default46 } from "./components/modal/ModalConfirm.vue.mjs";
|
|
48
49
|
import { DC_SLOT_ATTR, SlotRegion, parseSlotRegion } from "./config/slot-region.mjs";
|
|
49
|
-
import { default as
|
|
50
|
+
import { default as default47 } from "./components/data-view/DataGridView.vue.mjs";
|
|
50
51
|
import { EXPORT_MAX_LIMIT, OPERATE_COLUMN_PROP, exportCSV, pickExportColumns } from "./utils/export.mjs";
|
|
51
52
|
import { FORM_CONFIG_SELECT_ALL_VALUE, getConfirmPasswordRule, passwordRule, setFormComponentType } from "./helpers/form.mjs";
|
|
52
53
|
import { FORM_ITEM_CHANGE_LOADING, NEST_LAYOUT_TIERS, ceilToTier, generateFormData, getBlurSubmit, getChangeSubmit, getDatePickerShortcuts, getEnterSubmit, getPlaceholder, getVModelSugar, parseFormData, resolveFormLayout, resolveLayoutSpan, setInputComponent, setSelectComponent, stringifyFormData, swiftFormItemConfig } from "./components/form/utils.mjs";
|
|
53
|
-
import { default as
|
|
54
|
-
import { default as
|
|
55
|
-
import { default as
|
|
56
|
-
import { default as
|
|
57
|
-
import { default as
|
|
58
|
-
import { default as
|
|
59
|
-
import { default as
|
|
60
|
-
import { default as
|
|
61
|
-
import { default as
|
|
62
|
-
import { default as
|
|
63
|
-
import { default as
|
|
64
|
-
import { default as
|
|
65
|
-
import { default as
|
|
66
|
-
import { default as
|
|
67
|
-
import { default as
|
|
68
|
-
import { default as
|
|
69
|
-
import { default as
|
|
70
|
-
import { default as
|
|
71
|
-
import { default as
|
|
72
|
-
import { default as
|
|
73
|
-
import { default as
|
|
54
|
+
import { default as default48 } from "./components/form/FormActiveFilter.vue.mjs";
|
|
55
|
+
import { default as default49 } from "./components/form/FormItemNestForm.vue.mjs";
|
|
56
|
+
import { default as default50 } from "./components/form/FormItemNestFormList.vue.mjs";
|
|
57
|
+
import { default as default51 } from "./components/form/FormMain.vue.mjs";
|
|
58
|
+
import { default as default52 } from "./components/form/FormSearch.vue.mjs";
|
|
59
|
+
import { default as default53 } from "./components/form/FormShadowCloneDispatch.vue.mjs";
|
|
60
|
+
import { default as default54 } from "./components/form/FormSortSwitch.vue.mjs";
|
|
61
|
+
import { default as default55 } from "./components/form/FormSortSwitchGroup.vue.mjs";
|
|
62
|
+
import { default as default56 } from "./components/form/FormSubmitPanel.vue.mjs";
|
|
63
|
+
import { default as default57 } from "./components/form/FormUpload.vue.mjs";
|
|
64
|
+
import { default as default58 } from "./components/form/FormUploadAudio.vue.mjs";
|
|
65
|
+
import { default as default59 } from "./components/form/FormUploadImage.vue.mjs";
|
|
66
|
+
import { default as default60 } from "./components/form/FormUploadVideo.vue.mjs";
|
|
67
|
+
import { default as default61 } from "./components/form/FormVerifyImage.vue.mjs";
|
|
68
|
+
import { default as default62 } from "./components/data-view/InfiniteListView.vue.mjs";
|
|
69
|
+
import { default as default63, default as default64 } from "./components/list-layout/ListLayout.vue.mjs";
|
|
70
|
+
import { default as default65 } from "./components/modal/ModalVideoPreview.vue.mjs";
|
|
71
|
+
import { default as default66 } from "./components/panel/PanelEditSwitch.vue.mjs";
|
|
72
|
+
import { default as default67 } from "./components/panel/PanelItem.vue.mjs";
|
|
73
|
+
import { default as default68 } from "./components/panel/PanelItemNestForm.vue.mjs";
|
|
74
|
+
import { default as default69 } from "./components/panel/PanelMain.vue.mjs";
|
|
74
75
|
import { ROUTE_MODULE_LEVEL, TabsMainReplaceQueryKey } from "./config/route.mjs";
|
|
75
|
-
import { default as
|
|
76
|
+
import { default as default70 } from "./components/slot-layout/SlotLayoutFlowAside.vue.mjs";
|
|
76
77
|
import { TABLE_COLUMN_KEY_OPERATE, TABLE_COLUMN_KEY_SELECTION } from "./components/table/constants.mjs";
|
|
77
|
-
import { default as
|
|
78
|
-
import { default as
|
|
79
|
-
import { default as
|
|
80
|
-
import { default as
|
|
81
|
-
import { default as
|
|
78
|
+
import { default as default71 } from "./components/table/TableMain.vue.mjs";
|
|
79
|
+
import { default as default72 } from "./components/table/TableSkeleton.vue.mjs";
|
|
80
|
+
import { default as default73 } from "./components/table/TableToolbar.vue.mjs";
|
|
81
|
+
import { default as default74 } from "./components/modal/VideoPreviewTrigger.vue.mjs";
|
|
82
|
+
import { default as default75 } from "./components/view-layout/ViewLayout.vue.mjs";
|
|
82
83
|
import { buildRhythmCss, useCoreThemeApply } from "./hooks/use-theme-apply.mjs";
|
|
83
84
|
import { checkLogin } from "./router/guard/login.mjs";
|
|
84
85
|
import { checkPermission } from "./router/guard/permission.mjs";
|
|
@@ -139,83 +140,84 @@ export {
|
|
|
139
140
|
BODY_CONTENT_VIEWPORT_HEIGHT,
|
|
140
141
|
BODY_CONTENT_VIEWPORT_INSET,
|
|
141
142
|
BODY_CONTENT_VIEWPORT_RECT,
|
|
143
|
+
default44 as BadgeMark,
|
|
142
144
|
default4 as BooleanTag,
|
|
143
145
|
CORE_BRIDGE_KEY,
|
|
144
146
|
CORE_VIEWPORT_DECOR_ZINDEX_ABOVE,
|
|
145
147
|
CORE_VIEWPORT_DECOR_ZINDEX_BELOW,
|
|
146
|
-
|
|
148
|
+
default45 as ConfirmModal,
|
|
147
149
|
DC_SLOT_ATTR,
|
|
148
|
-
|
|
150
|
+
default47 as DataGridView,
|
|
149
151
|
default5 as DetailModal,
|
|
150
152
|
EXPORT_MAX_LIMIT,
|
|
151
153
|
FORM_CONFIG_SELECT_ALL_VALUE,
|
|
152
154
|
FORM_ITEM_CHANGE_LOADING,
|
|
153
155
|
FORM_NEST_REGISTRY_KEY,
|
|
154
|
-
|
|
156
|
+
default48 as FormActiveFilter,
|
|
155
157
|
default7 as FormInput,
|
|
156
158
|
default8 as FormInputShadowClone,
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
159
|
+
default49 as FormItemNestForm,
|
|
160
|
+
default50 as FormItemNestFormList,
|
|
161
|
+
default51 as FormMain,
|
|
160
162
|
default9 as FormModal,
|
|
161
163
|
default11 as FormRadioGroup,
|
|
162
|
-
|
|
164
|
+
default52 as FormSearch,
|
|
163
165
|
default12 as FormSelect,
|
|
164
166
|
default13 as FormSelectShadowClone,
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
167
|
+
default53 as FormShadowCloneDispatch,
|
|
168
|
+
default54 as FormSortSwitch,
|
|
169
|
+
default55 as FormSortSwitchGroup,
|
|
168
170
|
default14 as FormSubmitBtn,
|
|
169
|
-
|
|
171
|
+
default56 as FormSubmitPanel,
|
|
170
172
|
default15 as FormTree,
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
173
|
+
default57 as FormUpload,
|
|
174
|
+
default58 as FormUploadAudio,
|
|
175
|
+
default59 as FormUploadImage,
|
|
176
|
+
default60 as FormUploadVideo,
|
|
175
177
|
default16 as FormVerifyCode,
|
|
176
|
-
|
|
178
|
+
default61 as FormVerifyImage,
|
|
177
179
|
GLOBAL_MODAL_REMOTE_KEY,
|
|
178
180
|
default17 as HeightProvider,
|
|
179
181
|
default18 as ImagePreviewTrigger,
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
182
|
+
default62 as InfiniteListView,
|
|
183
|
+
default63 as ListLayout,
|
|
184
|
+
default64 as ListPage,
|
|
183
185
|
default19 as MenuItemSub,
|
|
184
186
|
default20 as MenuTree,
|
|
185
|
-
|
|
187
|
+
default46 as ModalConfirm,
|
|
186
188
|
default6 as ModalDetail,
|
|
187
189
|
default10 as ModalForm,
|
|
188
190
|
default21 as ModalImagePreview,
|
|
189
191
|
default22 as ModalPorter,
|
|
190
192
|
default23 as ModalShelf,
|
|
191
|
-
|
|
193
|
+
default65 as ModalVideoPreview,
|
|
192
194
|
NEST_LAYOUT_SCALE_KEY,
|
|
193
195
|
NEST_LAYOUT_TIERS,
|
|
194
196
|
OPERATE_COLUMN_PROP,
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
197
|
+
default66 as PanelEditSwitch,
|
|
198
|
+
default67 as PanelItem,
|
|
199
|
+
default68 as PanelItemNestForm,
|
|
198
200
|
default24 as PanelItemNestPanel,
|
|
199
|
-
|
|
201
|
+
default69 as PanelMain,
|
|
200
202
|
ROUTE_MODULE_LEVEL,
|
|
201
203
|
SCOPE_VIEWPORT_MAX_HEIGHT,
|
|
202
204
|
default25 as SelectModule,
|
|
203
205
|
default26 as ShadowClone,
|
|
204
|
-
|
|
206
|
+
default70 as SlotLayoutFlowAside,
|
|
205
207
|
default27 as SlotLayoutTemplate,
|
|
206
208
|
SlotRegion,
|
|
207
209
|
TABLE_COLUMN_KEY_OPERATE,
|
|
208
210
|
TABLE_COLUMN_KEY_SELECTION,
|
|
209
211
|
TABS_MAIN_VISUAL_LEVEL,
|
|
210
212
|
default28 as TableEmpty,
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
213
|
+
default71 as TableMain,
|
|
214
|
+
default72 as TableSkeleton,
|
|
215
|
+
default73 as TableToolbar,
|
|
214
216
|
default29 as TabsMain,
|
|
215
217
|
TabsMainReplaceQueryKey,
|
|
216
218
|
default30 as TriggerAutoImport,
|
|
217
|
-
|
|
218
|
-
|
|
219
|
+
default74 as VideoPreviewTrigger,
|
|
220
|
+
default75 as ViewLayout,
|
|
219
221
|
default31 as WatchSize,
|
|
220
222
|
buildRhythmCss,
|
|
221
223
|
ceilToTier,
|
package/es/store/app.mjs
CHANGED
|
@@ -62,12 +62,22 @@ function createAppStore(deps) {
|
|
|
62
62
|
return ((_a = pageLayout.value) == null ? void 0 : _a.bodyShimPadding) ?? bridge.APP_LAYOUT_BODY_CONFIG.shimPadding;
|
|
63
63
|
}
|
|
64
64
|
);
|
|
65
|
+
const viewportConfigEffective = computed(
|
|
66
|
+
() => {
|
|
67
|
+
var _a;
|
|
68
|
+
return ((_a = pageLayout.value) == null ? void 0 : _a.noViewportPadding) ? { paddingTop: 0, paddingRight: 0, paddingBottom: 0, paddingLeft: 0 } : bridge.APP_LAYOUT_VIEWPORT_CONFIG;
|
|
69
|
+
}
|
|
70
|
+
);
|
|
65
71
|
const viewportPadding = computed(() => ({
|
|
66
|
-
top: `${
|
|
67
|
-
right: `${
|
|
68
|
-
bottom: `${
|
|
69
|
-
left: `${
|
|
72
|
+
top: `${viewportConfigEffective.value.paddingTop}px`,
|
|
73
|
+
right: `${viewportConfigEffective.value.paddingRight}px`,
|
|
74
|
+
bottom: `${viewportConfigEffective.value.paddingBottom}px`,
|
|
75
|
+
left: `${viewportConfigEffective.value.paddingLeft}px`
|
|
70
76
|
}));
|
|
77
|
+
const noAnimation = computed(() => {
|
|
78
|
+
var _a;
|
|
79
|
+
return ((_a = pageLayout.value) == null ? void 0 : _a.noAnimation) ?? false;
|
|
80
|
+
});
|
|
71
81
|
const gapV = computed(() => bridge.APP_LAYOUT_GAP_CONFIG.size);
|
|
72
82
|
const gpx = computed(() => `${gapV.value}px`);
|
|
73
83
|
const headerStyle = computed(() => {
|
|
@@ -194,6 +204,8 @@ function createAppStore(deps) {
|
|
|
194
204
|
bodyShimPadding,
|
|
195
205
|
bodyShimStyle,
|
|
196
206
|
viewportPadding,
|
|
207
|
+
viewportConfigEffective,
|
|
208
|
+
noAnimation,
|
|
197
209
|
// 折叠态读 bridge 真相(架构反转,名字保留向后兼容);aside 对称新增。
|
|
198
210
|
// sidebar 侧经 sidebarCollapsedEffective 单点(分组形态闸为 false,见其注释)。
|
|
199
211
|
sidebarIsCollapse: sidebarCollapsedEffective,
|
package/es/style.css
CHANGED
|
@@ -63,6 +63,13 @@
|
|
|
63
63
|
justify-content: flex-end;
|
|
64
64
|
gap: 8px;
|
|
65
65
|
}
|
|
66
|
+
.badge-mark[data-v-12868883] .el-badge__content.is-fixed {
|
|
67
|
+
right: 0;
|
|
68
|
+
transform: translateY(-50%) translateX(calc(100% + 2px));
|
|
69
|
+
}
|
|
70
|
+
.badge-mark_middle[data-v-12868883] .el-badge__content.is-fixed {
|
|
71
|
+
top: 50%;
|
|
72
|
+
}
|
|
66
73
|
.form-item-tip[data-v-8b508d64] {
|
|
67
74
|
text-align: left;
|
|
68
75
|
line-height: 1.5;
|
|
@@ -220,7 +227,7 @@
|
|
|
220
227
|
height: 100%;
|
|
221
228
|
background-color: #f4f4f4;
|
|
222
229
|
}
|
|
223
|
-
.tabs-main-bar[data-v-
|
|
230
|
+
.tabs-main-bar[data-v-1f3b9f99] {
|
|
224
231
|
position: relative;
|
|
225
232
|
display: flex;
|
|
226
233
|
align-items: center;
|
|
@@ -230,37 +237,37 @@
|
|
|
230
237
|
/* ============ Variant: indicator-compact —— 紧凑 1px 下划线 ============ */
|
|
231
238
|
/* ============ Variant: plain —— 无装饰(active 加粗 + primary 字色) ============ */
|
|
232
239
|
}
|
|
233
|
-
.tabs-main-bar-item[data-v-
|
|
240
|
+
.tabs-main-bar-item[data-v-1f3b9f99] {
|
|
234
241
|
border: none;
|
|
235
242
|
background: transparent;
|
|
236
243
|
cursor: pointer;
|
|
237
244
|
color: var(--el-text-color-regular);
|
|
238
245
|
transition: background 0.2s, color 0.2s, box-shadow 0.2s;
|
|
239
246
|
}
|
|
240
|
-
.tabs-main-bar-indicator[data-v-
|
|
247
|
+
.tabs-main-bar-indicator[data-v-1f3b9f99] {
|
|
241
248
|
position: absolute;
|
|
242
249
|
left: 0;
|
|
243
250
|
background: var(--dc-core-primary-color);
|
|
244
251
|
transition: transform 0.2s, width 0.2s;
|
|
245
252
|
}
|
|
246
|
-
.tabs-main-bar--l1 .tabs-main-bar-item[data-v-
|
|
253
|
+
.tabs-main-bar--l1 .tabs-main-bar-item[data-v-1f3b9f99] {
|
|
247
254
|
font-size: 16px;
|
|
248
255
|
}
|
|
249
|
-
.tabs-main-bar--l2 .tabs-main-bar-item[data-v-
|
|
256
|
+
.tabs-main-bar--l2 .tabs-main-bar-item[data-v-1f3b9f99] {
|
|
250
257
|
font-size: 14px;
|
|
251
258
|
}
|
|
252
|
-
.tabs-main-bar--l3 .tabs-main-bar-item[data-v-
|
|
259
|
+
.tabs-main-bar--l3 .tabs-main-bar-item[data-v-1f3b9f99] {
|
|
253
260
|
font-size: 12px;
|
|
254
261
|
}
|
|
255
|
-
.tabs-main-bar--l4 .tabs-main-bar-item[data-v-
|
|
262
|
+
.tabs-main-bar--l4 .tabs-main-bar-item[data-v-1f3b9f99] {
|
|
256
263
|
font-size: 12px;
|
|
257
264
|
}
|
|
258
|
-
.tabs-main-bar--variant-card[data-v-
|
|
265
|
+
.tabs-main-bar--variant-card[data-v-1f3b9f99] {
|
|
259
266
|
position: relative;
|
|
260
267
|
overflow: hidden;
|
|
261
268
|
gap: 2px;
|
|
262
269
|
}
|
|
263
|
-
.tabs-main-bar--variant-card .tabs-main-bar-item[data-v-
|
|
270
|
+
.tabs-main-bar--variant-card .tabs-main-bar-item[data-v-1f3b9f99] {
|
|
264
271
|
position: relative;
|
|
265
272
|
padding: 8px 20px;
|
|
266
273
|
line-height: 1.4;
|
|
@@ -268,17 +275,17 @@
|
|
|
268
275
|
border-bottom: 0;
|
|
269
276
|
border-radius: 6px 6px 0 0;
|
|
270
277
|
}
|
|
271
|
-
.tabs-main-bar--variant-card .tabs-main-bar-item[data-v-
|
|
278
|
+
.tabs-main-bar--variant-card .tabs-main-bar-item[data-v-1f3b9f99]:hover:not(.tabs-main-bar-item_active) {
|
|
272
279
|
color: var(--dc-core-primary-color);
|
|
273
280
|
}
|
|
274
|
-
.tabs-main-bar--variant-card .tabs-main-bar-item_active[data-v-
|
|
281
|
+
.tabs-main-bar--variant-card .tabs-main-bar-item_active[data-v-1f3b9f99] {
|
|
275
282
|
color: var(--dc-core-primary-color);
|
|
276
283
|
border-color: var(--el-border-color-light);
|
|
277
284
|
/* active item 两侧各延伸 100vw 灰线,共同模拟"全宽 separator 在 active item 处中断"
|
|
278
285
|
active item 自身宽度内无线 → 视觉打通下方 content,完全 bg 无关 */
|
|
279
286
|
}
|
|
280
|
-
.tabs-main-bar--variant-card .tabs-main-bar-item_active[data-v-
|
|
281
|
-
.tabs-main-bar--variant-card .tabs-main-bar-item_active[data-v-
|
|
287
|
+
.tabs-main-bar--variant-card .tabs-main-bar-item_active[data-v-1f3b9f99]::before,
|
|
288
|
+
.tabs-main-bar--variant-card .tabs-main-bar-item_active[data-v-1f3b9f99]::after {
|
|
282
289
|
content: "";
|
|
283
290
|
position: absolute;
|
|
284
291
|
bottom: 0;
|
|
@@ -286,58 +293,58 @@
|
|
|
286
293
|
height: 1px;
|
|
287
294
|
background: var(--el-border-color-light);
|
|
288
295
|
}
|
|
289
|
-
.tabs-main-bar--variant-card .tabs-main-bar-item_active[data-v-
|
|
296
|
+
.tabs-main-bar--variant-card .tabs-main-bar-item_active[data-v-1f3b9f99]::before {
|
|
290
297
|
right: 100%;
|
|
291
298
|
/* 从 active item 左缘向左延伸 */
|
|
292
299
|
}
|
|
293
|
-
.tabs-main-bar--variant-card .tabs-main-bar-item_active[data-v-
|
|
300
|
+
.tabs-main-bar--variant-card .tabs-main-bar-item_active[data-v-1f3b9f99]::after {
|
|
294
301
|
left: 100%;
|
|
295
302
|
/* 从 active item 右缘向右延伸 */
|
|
296
303
|
}
|
|
297
|
-
.tabs-main-bar--variant-indicator .tabs-main-bar-item[data-v-
|
|
304
|
+
.tabs-main-bar--variant-indicator .tabs-main-bar-item[data-v-1f3b9f99] {
|
|
298
305
|
padding: 8px 16px;
|
|
299
306
|
}
|
|
300
|
-
.tabs-main-bar--variant-indicator .tabs-main-bar-item_active[data-v-
|
|
307
|
+
.tabs-main-bar--variant-indicator .tabs-main-bar-item_active[data-v-1f3b9f99] {
|
|
301
308
|
color: var(--dc-core-primary-color);
|
|
302
309
|
}
|
|
303
|
-
.tabs-main-bar--variant-indicator .tabs-main-bar-indicator[data-v-
|
|
310
|
+
.tabs-main-bar--variant-indicator .tabs-main-bar-indicator[data-v-1f3b9f99] {
|
|
304
311
|
bottom: 0;
|
|
305
312
|
height: 2px;
|
|
306
313
|
}
|
|
307
|
-
.tabs-main-bar--variant-indicator-compact .tabs-main-bar-item[data-v-
|
|
314
|
+
.tabs-main-bar--variant-indicator-compact .tabs-main-bar-item[data-v-1f3b9f99] {
|
|
308
315
|
padding: 6px 10px;
|
|
309
316
|
}
|
|
310
|
-
.tabs-main-bar--variant-indicator-compact .tabs-main-bar-item_active[data-v-
|
|
317
|
+
.tabs-main-bar--variant-indicator-compact .tabs-main-bar-item_active[data-v-1f3b9f99] {
|
|
311
318
|
color: var(--dc-core-primary-color);
|
|
312
319
|
}
|
|
313
|
-
.tabs-main-bar--variant-indicator-compact .tabs-main-bar-indicator[data-v-
|
|
320
|
+
.tabs-main-bar--variant-indicator-compact .tabs-main-bar-indicator[data-v-1f3b9f99] {
|
|
314
321
|
bottom: 0;
|
|
315
322
|
height: 1px;
|
|
316
323
|
}
|
|
317
|
-
.tabs-main-bar--variant-plain .tabs-main-bar-item[data-v-
|
|
324
|
+
.tabs-main-bar--variant-plain .tabs-main-bar-item[data-v-1f3b9f99] {
|
|
318
325
|
padding: 4px 8px;
|
|
319
326
|
}
|
|
320
|
-
.tabs-main-bar--variant-plain .tabs-main-bar-item_active[data-v-
|
|
327
|
+
.tabs-main-bar--variant-plain .tabs-main-bar-item_active[data-v-1f3b9f99] {
|
|
321
328
|
color: var(--dc-core-primary-color);
|
|
322
329
|
font-weight: 600;
|
|
323
330
|
}
|
|
324
|
-
.dc-tabs-tile--nav-absolute[data-v-
|
|
325
|
-
.dc-tabs-tile--nav-fixed[data-v-
|
|
331
|
+
.dc-tabs-tile--nav-absolute[data-v-acb4bb13],
|
|
332
|
+
.dc-tabs-tile--nav-fixed[data-v-acb4bb13] {
|
|
326
333
|
position: relative;
|
|
327
334
|
}
|
|
328
|
-
.dc-tabs-tile--nav-sticky[data-v-
|
|
335
|
+
.dc-tabs-tile--nav-sticky[data-v-acb4bb13] {
|
|
329
336
|
display: flex;
|
|
330
337
|
flex-direction: column;
|
|
331
338
|
}
|
|
332
|
-
.dc-tabs-tile__scroll[data-v-
|
|
339
|
+
.dc-tabs-tile__scroll[data-v-acb4bb13] {
|
|
333
340
|
display: flex;
|
|
334
341
|
flex-direction: column;
|
|
335
342
|
gap: 12px;
|
|
336
343
|
}
|
|
337
|
-
.dc-tabs-tile__card[data-v-
|
|
344
|
+
.dc-tabs-tile__card[data-v-acb4bb13] {
|
|
338
345
|
width: 100%;
|
|
339
346
|
}
|
|
340
|
-
.dc-tabs-tile__nav[data-v-
|
|
347
|
+
.dc-tabs-tile__nav[data-v-acb4bb13] {
|
|
341
348
|
z-index: 2;
|
|
342
349
|
max-height: 60%;
|
|
343
350
|
overflow: auto;
|
|
@@ -442,7 +449,7 @@
|
|
|
442
449
|
height: 100%;
|
|
443
450
|
padding: 0 20px;
|
|
444
451
|
}
|
|
445
|
-
.app-header[data-v-
|
|
452
|
+
.app-header[data-v-40b13903] {
|
|
446
453
|
transition-duration: 0.3s;
|
|
447
454
|
transition-property: height;
|
|
448
455
|
transition-timing-function: linear;
|
|
@@ -450,13 +457,13 @@
|
|
|
450
457
|
box-shadow: var(--dc-core-shadow, var(--el-box-shadow));
|
|
451
458
|
z-index: 1;
|
|
452
459
|
}
|
|
453
|
-
.app-header-module[data-v-
|
|
460
|
+
.app-header-module[data-v-40b13903] {
|
|
454
461
|
display: flex;
|
|
455
462
|
flex: 1;
|
|
456
463
|
height: 100%;
|
|
457
464
|
overflow-x: auto;
|
|
458
465
|
}
|
|
459
|
-
.app-header-module-item[data-v-
|
|
466
|
+
.app-header-module-item[data-v-40b13903] {
|
|
460
467
|
display: flex;
|
|
461
468
|
justify-content: center;
|
|
462
469
|
align-items: center;
|
|
@@ -464,24 +471,24 @@
|
|
|
464
471
|
white-space: nowrap;
|
|
465
472
|
height: 100%;
|
|
466
473
|
}
|
|
467
|
-
.app-header-module-item[data-v-
|
|
468
|
-
.app-header-module-item_active[data-v-
|
|
469
|
-
color: var(--
|
|
474
|
+
.app-header-module-item[data-v-40b13903]:hover,
|
|
475
|
+
.app-header-module-item_active[data-v-40b13903] {
|
|
476
|
+
color: var(--v05959ac0);
|
|
470
477
|
}
|
|
471
|
-
.app-header-module-item_active[data-v-
|
|
478
|
+
.app-header-module-item_active[data-v-40b13903] {
|
|
472
479
|
position: relative;
|
|
473
480
|
}
|
|
474
|
-
.app-header-module-item_active[data-v-
|
|
481
|
+
.app-header-module-item_active[data-v-40b13903]::before {
|
|
475
482
|
content: "";
|
|
476
483
|
position: absolute;
|
|
477
484
|
left: 50%;
|
|
478
485
|
top: 0;
|
|
479
486
|
width: 100%;
|
|
480
487
|
height: 3px;
|
|
481
|
-
background-color: var(--
|
|
488
|
+
background-color: var(--v05959ac0);
|
|
482
489
|
transform: translateX(-50%);
|
|
483
490
|
}
|
|
484
|
-
.app-header-shim[data-v-
|
|
491
|
+
.app-header-shim[data-v-40b13903] {
|
|
485
492
|
width: 100%;
|
|
486
493
|
height: 100%;
|
|
487
494
|
display: flex;
|
|
@@ -491,10 +498,10 @@
|
|
|
491
498
|
font-size: 18px;
|
|
492
499
|
font-weight: bold;
|
|
493
500
|
}
|
|
494
|
-
.app-header-module-item__icon[data-v-
|
|
501
|
+
.app-header-module-item__icon[data-v-40b13903] {
|
|
495
502
|
margin-right: 8px;
|
|
496
503
|
}
|
|
497
|
-
.app-header-right[data-v-
|
|
504
|
+
.app-header-right[data-v-40b13903] {
|
|
498
505
|
padding-left: 80px;
|
|
499
506
|
display: flex;
|
|
500
507
|
align-items: center;
|
|
@@ -593,13 +600,21 @@
|
|
|
593
600
|
.dc-modal-video-preview__overlay {
|
|
594
601
|
background: var(--el-overlay-color-dark, rgba(0, 0, 0, 0.7));
|
|
595
602
|
}
|
|
596
|
-
.app-layout[data-v-
|
|
603
|
+
.app-layout[data-v-b68520cd] {
|
|
597
604
|
position: relative;
|
|
598
605
|
}
|
|
599
|
-
.app-layout_noLogin[data-v-
|
|
600
|
-
.app-
|
|
601
|
-
.app-layout_noLogin[data-v-
|
|
602
|
-
.app-
|
|
606
|
+
.app-layout_noLogin[data-v-b68520cd] .app-body,
|
|
607
|
+
.app-layout_noAnimation[data-v-b68520cd] .app-body,
|
|
608
|
+
.app-layout_noLogin[data-v-b68520cd] .app-sidebar,
|
|
609
|
+
.app-layout_noAnimation[data-v-b68520cd] .app-sidebar,
|
|
610
|
+
.app-layout_noLogin[data-v-b68520cd] .app-aside,
|
|
611
|
+
.app-layout_noAnimation[data-v-b68520cd] .app-aside,
|
|
612
|
+
.app-layout_noLogin[data-v-b68520cd] .app-header,
|
|
613
|
+
.app-layout_noAnimation[data-v-b68520cd] .app-header,
|
|
614
|
+
.app-layout_noLogin[data-v-b68520cd] .app-footer,
|
|
615
|
+
.app-layout_noAnimation[data-v-b68520cd] .app-footer,
|
|
616
|
+
.app-layout_noLogin[data-v-b68520cd] .app-collapse-toggle,
|
|
617
|
+
.app-layout_noAnimation[data-v-b68520cd] .app-collapse-toggle {
|
|
603
618
|
transition: none !important;
|
|
604
619
|
}
|
|
605
620
|
.app-page[data-v-2359c6df] {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@done-coding/admin-core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.24.1-alpha.0",
|
|
4
4
|
"description": "内部前端库",
|
|
5
5
|
"private": false,
|
|
6
6
|
"main": "es/index.mjs",
|
|
@@ -83,5 +83,5 @@
|
|
|
83
83
|
"dependencies": {
|
|
84
84
|
"@tanstack/vue-virtual": "^3.13.35"
|
|
85
85
|
},
|
|
86
|
-
"gitHead": "
|
|
86
|
+
"gitHead": "368c62d7d673a7ad48665fd98147d133db6fbb09"
|
|
87
87
|
}
|
package/src/bridge/README.md
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
# bridge 模块(接线中枢)
|
|
2
2
|
|
|
3
|
-
`createCoreBridge(options)` 产出 `CoreBridge` 单例——认证 / 布局 / 主题 / 路由派生的总接线口,经 `app.use(bridge)` 全局注入(`CORE_BRIDGE_KEY`)。
|
|
3
|
+
`createCoreBridge(options)` 产出 `CoreBridge` 单例——认证 / 布局 / 主题 / 徽标 / 路由派生的总接线口,经 `app.use(bridge)` 全局注入(`CORE_BRIDGE_KEY`)。
|
|
4
4
|
|
|
5
5
|
> 详情见 [docs/README.md](./docs/README.md)(导出面 / 接线范式 / 认证与请求接线)
|
|
@@ -9,7 +9,8 @@ core 的「实例化中枢」:`createCoreBridge(options)` 产出 `CoreBridge`
|
|
|
9
9
|
| 符号 | 职责 |
|
|
10
10
|
| --- | --- |
|
|
11
11
|
| `createCoreBridge` | 工厂:传入 `CoreBridgeInitOptions`(配置 schema + auth facade + 存储 + 主题初值等),产出 `CoreBridge` |
|
|
12
|
-
| `CoreBridge` | 实例接口:`update()` 响应式更新 8 组配置视图 + `theme` 模块 + `layout` 折叠模块 + auth/api/路由派生方法 |
|
|
12
|
+
| `CoreBridge` | 实例接口:`update()` 响应式更新 8 组配置视图 + `theme` 模块 + `layout` 折叠模块 + `badge` 徽标注册表 + auth/api/路由派生方法 |
|
|
13
|
+
| `BadgeEntry` / `CoreBadgeModule` | 徽标注册表契约(见下「徽标注册表」章节) |
|
|
13
14
|
| `CoreBridgeRegisterOptions` | 注册选项类型(配置键 → 默认值/访问器) |
|
|
14
15
|
| `AppConfig` / `AppEnvInfo` | 配置 schema 顶层类型 |
|
|
15
16
|
| `AppLayout*Config`(Header/Footer/Sidebar/Aside/Body/Viewport/Gap) | 布局 7 组配置视图类型(`update` 改它们即响应式生效) |
|
|
@@ -19,7 +20,7 @@ core 的「实例化中枢」:`createCoreBridge(options)` 产出 `CoreBridge`
|
|
|
19
20
|
| `UserInfoAccess` / `UserInfoFieldAccessor` / `UserInfoPermissionAccessor` | userInfo 字段/权限访问器契约 |
|
|
20
21
|
| `AppBaseInfo` 等类型别名 | 各配置视图的便捷类型 |
|
|
21
22
|
|
|
22
|
-
**模块拆分**(规则 12:按耦合度独立文件):`theme/`(createBridgeThemeHook:风格 map + setStyle/setDark + 响应式 + 持久化)、`layout.ts`(折叠)、`plugin.ts`(插件注册)、`config-hook.ts`、`route.ts`(路由派生)、`state.ts`(useState 工厂)、`storage.ts`(命名空间存储)。
|
|
23
|
+
**模块拆分**(规则 12:按耦合度独立文件):`theme/`(createBridgeThemeHook:风格 map + setStyle/setDark + 响应式 + 持久化)、`layout.ts`(折叠)、`badge.ts`(徽标注册表)、`plugin.ts`(插件注册)、`config-hook.ts`、`route.ts`(路由派生)、`state.ts`(useState 工厂)、`storage.ts`(命名空间存储)。
|
|
23
24
|
|
|
24
25
|
## 快速上手(接线范式)
|
|
25
26
|
|
|
@@ -49,6 +50,101 @@ appCoreBridge.theme.setStyle("studio-stage");
|
|
|
49
50
|
appCoreBridge.theme.setDark(true);
|
|
50
51
|
```
|
|
51
52
|
|
|
53
|
+
## 徽标注册表(`bridge.badge`)
|
|
54
|
+
|
|
55
|
+
一张扁平的 `key → 标记` 表;菜单项与 `ActionBtn` 按 key 查表渲染**绝对定位角标**(跟在文案右侧,**不压字**,且[MUST] 不改变宿主尺寸)。
|
|
56
|
+
|
|
57
|
+
**分工**:core **只负责展示**——聚合、归属、清标、数据来源全部在 app。
|
|
58
|
+
|
|
59
|
+
### 契约
|
|
60
|
+
|
|
61
|
+
```ts
|
|
62
|
+
type BadgeEntry = {
|
|
63
|
+
/** 展示截止的绝对时间(epoch ms);缺省或 0 = 永久展示 */
|
|
64
|
+
expireAt?: number;
|
|
65
|
+
/** 面向 agent / AI 对话的语义描述(core 不消费,纯透传;自由文本) */
|
|
66
|
+
description?: string;
|
|
67
|
+
} & (
|
|
68
|
+
| { kind: "count"; value: number; max?: number } // 数字角标
|
|
69
|
+
| { kind: "new" } // 新上线
|
|
70
|
+
| { kind: "updated" } // 更新
|
|
71
|
+
| { kind: "beta" } // 内测:能用,不稳定
|
|
72
|
+
| { kind: "wip" } // 开发:不可用(可用性由 app 自行处置)
|
|
73
|
+
| { kind: "custom"; text: string; type?: BadgeProps["type"] }
|
|
74
|
+
);
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
内置五档的文案与语义色由 core 统一定,保证跨 app 一致;`custom` 只在内置档不够用时用。
|
|
78
|
+
|
|
79
|
+
`custom.type` 取 element-plus 的语义色档(`primary` / `success` / `warning` / `danger` / `info`),随主题亮暗自适配。[MUST NOT] 传色值字面量——规则 14,写死色切主题不响应。
|
|
80
|
+
|
|
81
|
+
### API
|
|
82
|
+
|
|
83
|
+
| 成员 | 用途 |
|
|
84
|
+
| --- | --- |
|
|
85
|
+
| `marks` | 全部标记(响应式只读;内部 `shallowRef`,每次操作产新对象) |
|
|
86
|
+
| `setAll(map)` | **整体替换** —— 轮询主路径,服务端本轮撤下的标随之消失 |
|
|
87
|
+
| `set(key, entry)` | 单点写入 / 覆盖 |
|
|
88
|
+
| `remove(key)` | 单点移除 |
|
|
89
|
+
|
|
90
|
+
⚠️ [MUST NOT] 用 `bridge.update()` 维护徽标:它是 `Object.assign` **浅合并**,轮询场景下服务端已撤下的标会永远留在表里。徽标独立走 `bridge.badge`。
|
|
91
|
+
|
|
92
|
+
### 两条失效路径(正交,可共存)
|
|
93
|
+
|
|
94
|
+
| 标记有无 `expireAt` | 怎么消失 | 谁负责 |
|
|
95
|
+
| --- | --- | --- |
|
|
96
|
+
| **有**(如随版本写死到某日的 NEW) | core 内部 60s tick 到点自动落 | core,app 不用管 |
|
|
97
|
+
| **无**(如服务端来的待办数 / Beta 标) | app 下一轮 `setAll` 刷掉 | app 的轮询节奏决定 |
|
|
98
|
+
|
|
99
|
+
`expireAt` **不是必填**——它是给「没有轮询通道」的静态标准备的兜底;有轮询的场景压根不用填。
|
|
100
|
+
|
|
101
|
+
### key 约定
|
|
102
|
+
|
|
103
|
+
| 消费点 | key 取值 |
|
|
104
|
+
| --- | --- |
|
|
105
|
+
| 菜单项(AppSidebar 菜单树 / AppHeader 模块条) | **路由 `path`**,无需另配 |
|
|
106
|
+
| `ActionBtn` | 显式 `badgeKey`(按钮没有 path) |
|
|
107
|
+
| `TabsMain` 的 tab 标题(tab-bar / tile 卡片 / tile 角落 nav 三处同步) | `TabsMainItem.badgeKey`。[MUST NOT] 复用 `item.key`——那是**局部唯一**(v-model 值 + 槽名),跨页会串标 |
|
|
108
|
+
|
|
109
|
+
### `description`:让这张表可被 agent 读
|
|
110
|
+
|
|
111
|
+
`description` 是 **core 完全不消费**的纯透传字段,供应用层做「本期上了什么功能」这类 AI 对话时,让模型知道某个 key 指向什么、怎么用、什么时候还不该用。
|
|
112
|
+
|
|
113
|
+
配合已有的 `kind`(= 变更类别,与 [Keep a Changelog](https://keepachangelog.com/) 的 Added / Changed 词汇同构)+ `expireAt`(时效),这张表本身就是一份**机器可读的功能公告**——业界的 Beamer / Headway / LaunchNotes 变更日志、Pendo / Appcues 功能采纳平台,维护的都是同构的 `{类别, 描述, 时效, 受众}` 条目。
|
|
114
|
+
|
|
115
|
+
```ts
|
|
116
|
+
bridge.badge.set("order.export", {
|
|
117
|
+
kind: "beta",
|
|
118
|
+
description:
|
|
119
|
+
"批量导出订单为 Excel。适合按月对账;单次超 10 万行会超时,内测期不建议用于生产对账。",
|
|
120
|
+
});
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
**约定:core 内给 agent 读的描述统一挂在 `description` 字段名上;形状按任务定。**
|
|
124
|
+
|
|
125
|
+
| 场景 | 类型 | 为什么 |
|
|
126
|
+
| --- | --- | --- |
|
|
127
|
+
| **选型**(主题 preset,35 套里挑一套) | `CoreConfigDescription` 的 `{suitable, unsuitable}` 正反 facets | 闭集选型;`unsuitable` 可做**无需 LLM 的廉价预筛**,也强迫作者写清排除条件 |
|
|
128
|
+
| **讲解**(徽标,说明某功能是什么) | 自由文本 `string` | 对齐 MCP / OpenAI function calling / JSON Schema 的 tool `description` 范式(均单 string,约定把「何时用 / 何时不该用」写在同一段)。讲解场景不做排除,拆正反会让「新功能的不宜选场景是什么」无从填起 |
|
|
129
|
+
|
|
130
|
+
[MUST NOT] 为「统一」立一个只含 `description` 的空壳基类让两边继承——形状本就不同,继承收益为零;且 agent 侧读描述最终都是拼进 prompt 的文本,形状差异几乎零成本。
|
|
131
|
+
|
|
132
|
+
⚠️ **边界**:重内容(图文步骤 / 深链 / 视频 / 前置条件)[SHOULD] 走 app 自己的知识库、用**同一 key 关联**,[MUST NOT] 往 `description` 堆——否则 `BadgeEntry` 会从「一个标」膨胀成 CMS 条目,core 被迫维护一个自己不消费的复杂 schema。
|
|
133
|
+
|
|
134
|
+
> 产品提醒:AI 主动推销「本期新功能」极易变成打扰(Clippy 陷阱)。要做 [SHOULD] 守低风险、可逆、ambient(不抢焦点)、用户始终在控制位。
|
|
135
|
+
|
|
136
|
+
### 已知限制([MUST] 知悉,非缺陷)
|
|
137
|
+
|
|
138
|
+
- **时钟源是客户端**:用户机器时间不准或被改,`expireAt` 判定会偏。徽标是装饰,未做服务端对时。
|
|
139
|
+
- **到期最多滞留 60s**:tick 粒度,非实时。全 app 共享一个定时器,非每标一个。
|
|
140
|
+
- **不做冒泡聚合**:core [MUST NOT] 把子级的标上卷到父级菜单,见下。
|
|
141
|
+
|
|
142
|
+
### 为什么不做冒泡聚合
|
|
143
|
+
|
|
144
|
+
聚合要求 core 知道「一个 entry 归属哪个路由」,而真实系统里这个归属是多对多且有例外——弹窗里的按钮、全局工具栏按钮、一个入口同时挂在两个模块下。core 一旦定义归属模型,就是在替业务定信息架构,属越权。
|
|
145
|
+
|
|
146
|
+
需要「一级模块上有引导点」的效果:**app 自己算完,直接往父级 path 写一条 entry**,同样达到效果且归属规则完全可控。
|
|
147
|
+
|
|
52
148
|
## 布局配置里的行为开关
|
|
53
149
|
|
|
54
150
|
布局 8 组配置多为几何数值(宽高 / 间距 / 配色),少数是**行为开关**——需要单独知道语义:
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
| `AppLayout` | 应用布局总控:bridge/appStore/userStore 三注入 + 5 内容锚 + 内置默认经典布局 shell | `bridge`(必填)`appStore`(必填)`userStore`(必填)`watermarkContent` `sidebarAboveHeader` | [README-AppLayout.md](./docs/README-AppLayout.md) |
|
|
11
11
|
| `AppPage` | 页面主体容器:撑满视口 + 四向悬浮 fixed 插槽 | `fullViewport` `fullMode` `gap` `topBg`/`bottomBg`/`leftBg`/`rightBg` `nextViewportMaxHeight` | [README-AppPage.md](./docs/README-AppPage.md) |
|
|
12
12
|
| `AppBody` | 内容主体:内置 RouterView + KeepAlive + 面包屑 affix + #footer 槽 | `menuFlatList` | [README-AppBody.md](./docs/README-AppBody.md) |
|
|
13
|
-
| `AppHeader` | 顶栏积木:#left 品牌 +
|
|
13
|
+
| `AppHeader` | 顶栏积木:#left 品牌 + 路由模块菜单(按 path 查 bridge.badge 显徽标)+ #right 操作区 | `menus` `menuFlatList`(必填)`show`(未消费) | [README-AppHeader.md](./docs/README-AppHeader.md) |
|
|
14
14
|
| `AppSidebar` | 侧边栏积木:折叠把手 + MenuTree 菜单(树形/分组双形态)+ #top/#bottom 高度自撑 | `show` `menus`(必填)`menuFlatList`(必填)`aboveHeader`;菜单形态读 bridge `APP_LAYOUT_SIDEBAR_CONFIG.menuGroupMode`(分组态无折叠把手) | [README-AppSidebar.md](./docs/README-AppSidebar.md) |
|
|
15
15
|
| `AppAside` | 右侧补充区积木(对称 AppSidebar),无内置内容 | `show` | [README-AppAside.md](./docs/README-AppAside.md) |
|
|
16
16
|
| `AppBreadcrumb` | 面包屑积木:menuFlatList 匹配 route.matched 出链 | `height`(必填)`menuFlatList`(必填) | [README-AppBreadcrumb.md](./docs/README-AppBreadcrumb.md) |
|