create-nativecore 0.1.0 → 0.2.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 +10 -18
- package/bin/index.mjs +407 -489
- package/package.json +4 -3
- package/template/.env.example +28 -0
- package/template/.htmlhintrc +14 -0
- package/template/api/data/dashboard.json +11 -0
- package/template/api/data/users.json +18 -0
- package/template/api/mockApi.js +161 -0
- package/template/assets/icon.svg +13 -0
- package/template/assets/logo.svg +25 -0
- package/template/eslint.config.js +94 -0
- package/template/index.html +137 -0
- package/template/manifest.json +19 -0
- package/template/public/.well-known/security.txt +9 -0
- package/template/public/_headers +24 -0
- package/template/public/_redirects +14 -0
- package/template/public/assets/icon.svg +13 -0
- package/template/public/assets/logo.svg +25 -0
- package/template/public/manifest.json +19 -0
- package/template/public/robots.txt +13 -0
- package/template/public/sitemap.xml +27 -0
- package/template/scripts/build-for-bots.mjs +121 -0
- package/template/scripts/convert-to-ts.mjs +106 -0
- package/template/scripts/fix-encoding.mjs +38 -0
- package/template/scripts/fix-svg-paths.mjs +32 -0
- package/template/scripts/generate-cf-router.mjs +52 -0
- package/template/scripts/inject-dev-tools.mjs +41 -0
- package/template/scripts/inject-version.mjs +65 -0
- package/template/scripts/make-component.mjs +445 -0
- package/template/scripts/make-component.mjs.backup +432 -0
- package/template/scripts/make-controller.mjs +119 -0
- package/template/scripts/make-core-component.mjs +303 -0
- package/template/scripts/make-view.mjs +346 -0
- package/template/scripts/minify.mjs +71 -0
- package/template/scripts/prepare-static-assets.mjs +141 -0
- package/template/scripts/prompt-bot-build.mjs +223 -0
- package/template/scripts/remove-component.mjs +170 -0
- package/template/scripts/remove-core-component.mjs +156 -0
- package/template/scripts/remove-dev.mjs +13 -0
- package/template/scripts/remove-view.mjs +200 -0
- package/template/scripts/strip-dev-blocks.mjs +30 -0
- package/template/scripts/watch-compile.mjs +69 -0
- package/template/server.js +1066 -0
- package/template/src/app.ts +115 -0
- package/template/src/components/appRegistry.ts +8 -0
- package/template/src/components/core/app-footer.ts +27 -0
- package/template/src/components/core/app-header.ts +175 -0
- package/template/src/components/core/app-sidebar.ts +238 -0
- package/template/src/components/core/loading-spinner.ts +25 -0
- package/template/src/components/core/nc-a.ts +313 -0
- package/template/src/components/core/nc-accordion.ts +186 -0
- package/template/src/components/core/nc-alert.ts +153 -0
- package/template/src/components/core/nc-animation.ts +1150 -0
- package/template/src/components/core/nc-autocomplete.ts +271 -0
- package/template/src/components/core/nc-avatar-group.ts +113 -0
- package/template/src/components/core/nc-avatar.ts +148 -0
- package/template/src/components/core/nc-badge.ts +86 -0
- package/template/src/components/core/nc-bottom-nav.ts +214 -0
- package/template/src/components/core/nc-breadcrumb.ts +96 -0
- package/template/src/components/core/nc-button.ts +307 -0
- package/template/src/components/core/nc-card.ts +160 -0
- package/template/src/components/core/nc-checkbox.ts +282 -0
- package/template/src/components/core/nc-chip.ts +115 -0
- package/template/src/components/core/nc-code.ts +314 -0
- package/template/src/components/core/nc-collapsible.ts +154 -0
- package/template/src/components/core/nc-color-picker.ts +268 -0
- package/template/src/components/core/nc-copy-button.ts +119 -0
- package/template/src/components/core/nc-date-picker.ts +443 -0
- package/template/src/components/core/nc-div.ts +280 -0
- package/template/src/components/core/nc-divider.ts +81 -0
- package/template/src/components/core/nc-drawer.ts +230 -0
- package/template/src/components/core/nc-dropdown.ts +178 -0
- package/template/src/components/core/nc-empty-state.ts +134 -0
- package/template/src/components/core/nc-file-upload.ts +354 -0
- package/template/src/components/core/nc-form.ts +312 -0
- package/template/src/components/core/nc-image.ts +184 -0
- package/template/src/components/core/nc-input.ts +383 -0
- package/template/src/components/core/nc-kbd.ts +48 -0
- package/template/src/components/core/nc-menu-item.ts +193 -0
- package/template/src/components/core/nc-menu.ts +376 -0
- package/template/src/components/core/nc-modal.ts +238 -0
- package/template/src/components/core/nc-nav-item.ts +151 -0
- package/template/src/components/core/nc-number-input.ts +350 -0
- package/template/src/components/core/nc-otp-input.ts +235 -0
- package/template/src/components/core/nc-pagination.ts +178 -0
- package/template/src/components/core/nc-popover.ts +260 -0
- package/template/src/components/core/nc-progress-circular.ts +119 -0
- package/template/src/components/core/nc-progress.ts +134 -0
- package/template/src/components/core/nc-radio.ts +235 -0
- package/template/src/components/core/nc-rating.ts +266 -0
- package/template/src/components/core/nc-rich-text.ts +283 -0
- package/template/src/components/core/nc-scroll-top.ts +116 -0
- package/template/src/components/core/nc-select.ts +452 -0
- package/template/src/components/core/nc-skeleton.ts +107 -0
- package/template/src/components/core/nc-slider.ts +285 -0
- package/template/src/components/core/nc-snackbar.ts +230 -0
- package/template/src/components/core/nc-splash.ts +343 -0
- package/template/src/components/core/nc-stepper.ts +247 -0
- package/template/src/components/core/nc-switch.ts +281 -0
- package/template/src/components/core/nc-tab-item.ts +138 -0
- package/template/src/components/core/nc-table.ts +279 -0
- package/template/src/components/core/nc-tabs.ts +554 -0
- package/template/src/components/core/nc-tag-input.ts +279 -0
- package/template/src/components/core/nc-textarea.ts +216 -0
- package/template/src/components/core/nc-time-picker.ts +438 -0
- package/template/src/components/core/nc-timeline.ts +186 -0
- package/template/src/components/core/nc-tooltip.ts +143 -0
- package/template/src/components/frameworkRegistry.ts +68 -0
- package/template/src/components/preloadRegistry.ts +28 -0
- package/template/src/components/registry.ts +8 -0
- package/template/src/components/ui/dashboard-signal-lab.ts +284 -0
- package/template/src/constants/apiEndpoints.ts +27 -0
- package/template/src/constants/errorMessages.ts +23 -0
- package/template/src/constants/index.ts +8 -0
- package/template/src/constants/routePaths.ts +15 -0
- package/template/src/constants/storageKeys.ts +18 -0
- package/template/src/controllers/dashboard.controller.ts +200 -0
- package/template/src/controllers/home.controller.ts +21 -0
- package/template/src/controllers/index.ts +11 -0
- package/template/src/controllers/login.controller.ts +131 -0
- package/template/src/core/component.ts +354 -0
- package/template/src/core/errorHandler.ts +85 -0
- package/template/src/core/gpu-animation.ts +604 -0
- package/template/src/core/http.ts +173 -0
- package/template/src/core/lazyComponents.ts +90 -0
- package/template/src/core/router.ts +642 -0
- package/template/src/core/signals.ts +146 -0
- package/template/src/core/state.ts +248 -0
- package/template/src/dev/component-editor.ts +1363 -0
- package/template/src/dev/component-overlay.ts +278 -0
- package/template/src/dev/context-menu.ts +223 -0
- package/template/src/dev/denc-tools.ts +250 -0
- package/template/src/dev/hmr.ts +189 -0
- package/template/src/dev/nfbs.code-workspace +27 -0
- package/template/src/dev/outline-panel.ts +1247 -0
- package/template/src/middleware/auth.middleware.ts +23 -0
- package/template/src/routes/routes.ts +38 -0
- package/template/src/services/api.service.ts +394 -0
- package/template/src/services/auth.service.ts +176 -0
- package/template/src/services/index.ts +8 -0
- package/template/src/services/logger.service.ts +74 -0
- package/template/src/services/storage.service.ts +88 -0
- package/template/src/stores/appStore.ts +57 -0
- package/template/src/stores/uiStore.ts +36 -0
- package/template/src/styles/core-variables.css +219 -0
- package/template/src/styles/core.css +710 -0
- package/template/src/styles/main.css +3164 -0
- package/template/src/styles/variables.css +152 -0
- package/template/src/types/global.d.ts +47 -0
- package/template/src/utils/cacheBuster.ts +20 -0
- package/template/src/utils/dom.ts +149 -0
- package/template/src/utils/events.ts +203 -0
- package/template/src/utils/form.ts +176 -0
- package/template/src/utils/formatters.ts +169 -0
- package/template/src/utils/helpers.ts +195 -0
- package/template/src/utils/markdown.ts +307 -0
- package/template/src/utils/sidebar.ts +96 -0
- package/template/src/utils/smoothScroll.ts +85 -0
- package/template/src/utils/templates.ts +23 -0
- package/template/src/utils/validation.ts +73 -0
- package/template/src/views/protected/dashboard.html +293 -0
- package/template/src/views/public/home.html +150 -0
- package/template/src/views/public/login.html +102 -0
- package/template/tests/unit/component.test.ts +87 -0
- package/template/tests/unit/computed.test.ts +79 -0
- package/template/tests/unit/form.test.ts +68 -0
- package/template/tests/unit/formatters.test.ts +49 -0
- package/template/tests/unit/lazy-components.test.ts +59 -0
- package/template/tests/unit/markdown.test.ts +62 -0
- package/template/tests/unit/router.test.ts +112 -0
- package/template/tests/unit/signals.test.ts +54 -0
- package/template/tests/unit/validation.test.ts +50 -0
- package/template/tsconfig.build.json +21 -0
- package/template/tsconfig.json +51 -0
- package/template/vitest.config.ts +36 -0
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* NcRating Component
|
|
3
|
+
*
|
|
4
|
+
* NativeCore Framework Core Component
|
|
5
|
+
*
|
|
6
|
+
* Attributes:
|
|
7
|
+
* - name: string — form field name
|
|
8
|
+
* - value: number — current rating (0 = none)
|
|
9
|
+
* - max: number — total stars (default: 5)
|
|
10
|
+
* - readonly: boolean — display only, no interaction
|
|
11
|
+
* - disabled: boolean — disabled state
|
|
12
|
+
* - size: 'sm' | 'md' | 'lg' (default: 'md')
|
|
13
|
+
* - variant: 'star' | 'heart' | 'circle' (default: 'star')
|
|
14
|
+
* - allow-clear: boolean — clicking the active star clears the value
|
|
15
|
+
*
|
|
16
|
+
* Events:
|
|
17
|
+
* - change: CustomEvent<{ value: number; name: string }>
|
|
18
|
+
*
|
|
19
|
+
* Usage:
|
|
20
|
+
* <nc-rating name="score" value="3"></nc-rating>
|
|
21
|
+
* <nc-rating name="score" value="4" max="10" size="lg" allow-clear></nc-rating>
|
|
22
|
+
* <nc-rating name="mood" variant="heart" value="2"></nc-rating>
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import { Component, defineComponent } from '@core/component.js';
|
|
26
|
+
|
|
27
|
+
const ICONS: Record<string, { filled: string; empty: string }> = {
|
|
28
|
+
star: {
|
|
29
|
+
filled: `<svg class="icon-filled" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" width="1em" height="1em"><path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/></svg>`,
|
|
30
|
+
empty: `<svg class="icon-empty" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" width="1em" height="1em"><path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/></svg>`,
|
|
31
|
+
},
|
|
32
|
+
heart: {
|
|
33
|
+
filled: `<svg class="icon-filled" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" width="1em" height="1em"><path d="M20.84 4.61a5.5 5.5 0 00-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 00-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 000-7.78z"/></svg>`,
|
|
34
|
+
empty: `<svg class="icon-empty" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" width="1em" height="1em"><path d="M20.84 4.61a5.5 5.5 0 00-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 00-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 000-7.78z"/></svg>`,
|
|
35
|
+
},
|
|
36
|
+
circle: {
|
|
37
|
+
filled: `<svg class="icon-filled" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" width="1em" height="1em"><circle cx="12" cy="12" r="10"/></svg>`,
|
|
38
|
+
empty: `<svg class="icon-empty" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" width="1em" height="1em"><circle cx="12" cy="12" r="10"/></svg>`,
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export class NcRating extends Component {
|
|
43
|
+
static useShadowDOM = true;
|
|
44
|
+
|
|
45
|
+
static attributeOptions = {
|
|
46
|
+
variant: ['star', 'heart', 'circle'],
|
|
47
|
+
size: ['sm', 'md', 'lg']
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
static get observedAttributes() {
|
|
51
|
+
// 'value' excluded — managed via _value to prevent base-class re-render on every pick
|
|
52
|
+
return ['name', 'max', 'readonly', 'disabled', 'size', 'variant', 'allow-clear'];
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
private _value = 0;
|
|
56
|
+
private _hovered = 0;
|
|
57
|
+
|
|
58
|
+
constructor() {
|
|
59
|
+
super();
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
private _getMax() { return Number(this.getAttribute('max') || 5); }
|
|
63
|
+
private _getValue() { return this._value; }
|
|
64
|
+
|
|
65
|
+
template() {
|
|
66
|
+
// Seed from attribute only on first mount
|
|
67
|
+
if (!this._mounted) {
|
|
68
|
+
this._value = Number(this.getAttribute('value') || 0);
|
|
69
|
+
}
|
|
70
|
+
const value = this._getValue();
|
|
71
|
+
const max = this._getMax();
|
|
72
|
+
const variant = this.getAttribute('variant') || 'star';
|
|
73
|
+
const icon = ICONS[variant] ?? ICONS.star;
|
|
74
|
+
const readonly = this.hasAttribute('readonly');
|
|
75
|
+
const disabled = this.hasAttribute('disabled');
|
|
76
|
+
const interactive = !readonly && !disabled;
|
|
77
|
+
|
|
78
|
+
// Both icons are always in the DOM — visibility toggled by CSS class only.
|
|
79
|
+
// This prevents _applyHover from ever replacing innerHTML, which would
|
|
80
|
+
// trigger spurious mouseover events and break the saved-state display.
|
|
81
|
+
const items = Array.from({ length: max }, (_, i) => {
|
|
82
|
+
const pos = i + 1;
|
|
83
|
+
const filled = pos <= value;
|
|
84
|
+
return `<span
|
|
85
|
+
class="item${filled ? ' filled' : ''}"
|
|
86
|
+
data-pos="${pos}"
|
|
87
|
+
role="${interactive ? 'radio' : 'presentation'}"
|
|
88
|
+
aria-checked="${filled}"
|
|
89
|
+
aria-label="${pos} of ${max}"
|
|
90
|
+
tabindex="${interactive ? '0' : '-1'}"
|
|
91
|
+
>${icon.filled}${icon.empty}</span>`;
|
|
92
|
+
}).join('');
|
|
93
|
+
|
|
94
|
+
return `
|
|
95
|
+
<style>
|
|
96
|
+
:host {
|
|
97
|
+
display: inline-flex;
|
|
98
|
+
align-items: center;
|
|
99
|
+
font-family: var(--nc-font-family);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
.items {
|
|
103
|
+
display: inline-flex;
|
|
104
|
+
align-items: center;
|
|
105
|
+
gap: 2px;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
:host,
|
|
109
|
+
:host([size="md"]) { font-size: 1.5rem; }
|
|
110
|
+
:host([size="sm"]) { font-size: 1rem; }
|
|
111
|
+
:host([size="lg"]) { font-size: 2rem; }
|
|
112
|
+
|
|
113
|
+
.item {
|
|
114
|
+
display: inline-flex;
|
|
115
|
+
align-items: center;
|
|
116
|
+
justify-content: center;
|
|
117
|
+
cursor: ${interactive ? 'pointer' : 'default'};
|
|
118
|
+
color: var(--nc-gray-300);
|
|
119
|
+
transition: color var(--nc-transition-fast), transform var(--nc-transition-fast);
|
|
120
|
+
opacity: ${disabled ? '0.4' : '1'};
|
|
121
|
+
line-height: 1;
|
|
122
|
+
pointer-events: ${interactive ? 'auto' : 'none'};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/* SVGs are siblings inside .item — show one at a time */
|
|
126
|
+
.item .icon-filled { display: none; }
|
|
127
|
+
.item .icon-empty { display: block; }
|
|
128
|
+
|
|
129
|
+
.item.filled .icon-filled { display: block; }
|
|
130
|
+
.item.filled .icon-empty { display: none; }
|
|
131
|
+
|
|
132
|
+
/* Hover/active state */
|
|
133
|
+
.item.filled,
|
|
134
|
+
.item.hovered {
|
|
135
|
+
color: var(--nc-warning, #f59e0b);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
.item.hovered {
|
|
139
|
+
transform: scale(1.2);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/* During hover preview, preview-filled shows filled icon */
|
|
143
|
+
.item.preview-filled .icon-filled { display: block; }
|
|
144
|
+
.item.preview-filled .icon-empty { display: none; }
|
|
145
|
+
.item.preview-empty .icon-filled { display: none; }
|
|
146
|
+
.item.preview-empty .icon-empty { display: block; }
|
|
147
|
+
|
|
148
|
+
.item:focus-visible {
|
|
149
|
+
outline: 2px solid var(--nc-primary);
|
|
150
|
+
outline-offset: 2px;
|
|
151
|
+
border-radius: 2px;
|
|
152
|
+
}
|
|
153
|
+
</style>
|
|
154
|
+
|
|
155
|
+
<div
|
|
156
|
+
class="items"
|
|
157
|
+
role="${interactive ? 'radiogroup' : 'img'}"
|
|
158
|
+
aria-label="Rating: ${value} of ${max}"
|
|
159
|
+
>${items}</div>
|
|
160
|
+
<input type="hidden" name="${this.getAttribute('name') || ''}" value="${value}" />
|
|
161
|
+
`;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
onMount() {
|
|
165
|
+
this._bindEvents();
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
private _bindEvents() {
|
|
169
|
+
if (this.hasAttribute('readonly') || this.hasAttribute('disabled')) return;
|
|
170
|
+
|
|
171
|
+
const container = this.$<HTMLElement>('.items')!;
|
|
172
|
+
|
|
173
|
+
container.addEventListener('mouseover', (e) => {
|
|
174
|
+
const item = (e.target as HTMLElement).closest<HTMLElement>('.item');
|
|
175
|
+
if (!item) return;
|
|
176
|
+
this._hovered = Number(item.dataset.pos);
|
|
177
|
+
this._applyState();
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
container.addEventListener('mouseleave', () => {
|
|
181
|
+
this._hovered = 0;
|
|
182
|
+
this._applyState();
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
container.addEventListener('click', (e) => {
|
|
186
|
+
const item = (e.target as HTMLElement).closest<HTMLElement>('.item');
|
|
187
|
+
if (!item) return;
|
|
188
|
+
const pos = Number(item.dataset.pos);
|
|
189
|
+
const next = this.hasAttribute('allow-clear') && pos === this._getValue() ? 0 : pos;
|
|
190
|
+
this._hovered = 0;
|
|
191
|
+
this._commit(next);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
this.$$<HTMLElement>('.item').forEach(item => {
|
|
195
|
+
item.addEventListener('keydown', (e: KeyboardEvent) => {
|
|
196
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
197
|
+
e.preventDefault();
|
|
198
|
+
item.click();
|
|
199
|
+
}
|
|
200
|
+
if (e.key === 'ArrowRight' || e.key === 'ArrowUp') {
|
|
201
|
+
e.preventDefault();
|
|
202
|
+
this._commit(Math.min(this._getMax(), this._getValue() + 1));
|
|
203
|
+
}
|
|
204
|
+
if (e.key === 'ArrowLeft' || e.key === 'ArrowDown') {
|
|
205
|
+
e.preventDefault();
|
|
206
|
+
this._commit(Math.max(0, this._getValue() - 1));
|
|
207
|
+
}
|
|
208
|
+
});
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Applies the current hover/selected state to the DOM via classes only — no innerHTML changes
|
|
213
|
+
private _applyState() {
|
|
214
|
+
const value = this._getValue();
|
|
215
|
+
const hovered = this._hovered;
|
|
216
|
+
|
|
217
|
+
this.$$<HTMLElement>('.item').forEach(item => {
|
|
218
|
+
const pos = Number(item.dataset.pos);
|
|
219
|
+
|
|
220
|
+
if (hovered > 0) {
|
|
221
|
+
// Preview mode: override fill display with preview classes
|
|
222
|
+
const previewFilled = pos <= hovered;
|
|
223
|
+
item.classList.toggle('hovered', previewFilled);
|
|
224
|
+
item.classList.toggle('preview-filled', previewFilled);
|
|
225
|
+
item.classList.toggle('preview-empty', !previewFilled);
|
|
226
|
+
item.setAttribute('aria-checked', String(pos <= value));
|
|
227
|
+
} else {
|
|
228
|
+
// Committed state: remove all preview classes, rely on .filled
|
|
229
|
+
item.classList.remove('hovered', 'preview-filled', 'preview-empty');
|
|
230
|
+
item.classList.toggle('filled', pos <= value);
|
|
231
|
+
item.setAttribute('aria-checked', String(pos <= value));
|
|
232
|
+
}
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
private _commit(value: number) {
|
|
237
|
+
this._value = value;
|
|
238
|
+
// setAttribute is safe — 'value' is not in observedAttributes, no re-render triggered
|
|
239
|
+
this.setAttribute('value', String(value));
|
|
240
|
+
|
|
241
|
+
const hidden = this.$<HTMLInputElement>('input[type="hidden"]');
|
|
242
|
+
if (hidden) hidden.value = String(value);
|
|
243
|
+
|
|
244
|
+
const container = this.$('.items');
|
|
245
|
+
if (container) container.setAttribute('aria-label', `Rating: ${value} of ${this._getMax()}`);
|
|
246
|
+
|
|
247
|
+
// Re-sync .filled classes to match the new committed value
|
|
248
|
+
this.$$<HTMLElement>('.item').forEach(item => {
|
|
249
|
+
item.classList.toggle('filled', Number(item.dataset.pos) <= value);
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
this.dispatchEvent(new CustomEvent('change', {
|
|
253
|
+
bubbles: true,
|
|
254
|
+
composed: true,
|
|
255
|
+
detail: { value, name: this.getAttribute('name') || '' }
|
|
256
|
+
}));
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
attributeChangedCallback(name: string, oldValue: string, newValue: string) {
|
|
260
|
+
if (oldValue === newValue || !this._mounted) return;
|
|
261
|
+
this.render();
|
|
262
|
+
this._bindEvents();
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
defineComponent('nc-rating', NcRating);
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* NcRichText Component
|
|
3
|
+
*
|
|
4
|
+
* A lightweight rich text editor built on contenteditable + execCommand,
|
|
5
|
+
* with a toolbar, HTML output, and zero external dependencies.
|
|
6
|
+
*
|
|
7
|
+
* Attributes:
|
|
8
|
+
* - name: string — for use in nc-form (emits HTML string as value)
|
|
9
|
+
* - value: string — initial HTML content
|
|
10
|
+
* - placeholder: string (default: 'Start typing...')
|
|
11
|
+
* - disabled: boolean
|
|
12
|
+
* - readonly: boolean
|
|
13
|
+
* - toolbar: string — comma-separated list of toolbar buttons to show.
|
|
14
|
+
* Full list: 'bold,italic,underline,strike,|,h1,h2,h3,|,ul,ol,|,blockquote,code,|,link,|,align-left,align-center,align-right,|,clear'
|
|
15
|
+
* Default: all of the above
|
|
16
|
+
* - min-height: string — CSS min-height of editable area (default: '120px')
|
|
17
|
+
* - max-height: string — CSS max-height of editable area (default: '400px')
|
|
18
|
+
*
|
|
19
|
+
* Events:
|
|
20
|
+
* - input: CustomEvent<{ value: string; name: string }>
|
|
21
|
+
* - change: CustomEvent<{ value: string; name: string }>
|
|
22
|
+
*
|
|
23
|
+
* Usage:
|
|
24
|
+
* <nc-rich-text name="body" placeholder="Write something..." min-height="200px"></nc-rich-text>
|
|
25
|
+
* <nc-rich-text name="notes" toolbar="bold,italic,underline,|,ul,ol"></nc-rich-text>
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
import { Component, defineComponent } from '@core/component.js';
|
|
29
|
+
|
|
30
|
+
const ICONS: Record<string, string> = {
|
|
31
|
+
bold: `<svg viewBox="0 0 16 16" fill="none" width="14" height="14"><text x="3" y="13" font-family="Georgia,serif" font-size="13" font-weight="bold" fill="currentColor">B</text></svg>`,
|
|
32
|
+
italic: `<svg viewBox="0 0 16 16" fill="none" width="14" height="14"><text x="4" y="13" font-family="Georgia,serif" font-size="13" font-style="italic" fill="currentColor">I</text></svg>`,
|
|
33
|
+
underline: `<svg viewBox="0 0 16 16" fill="none" width="14" height="14"><text x="3" y="11" font-family="Arial,sans-serif" font-size="12" text-decoration="underline" fill="currentColor">U</text><line x1="2" y1="14" x2="14" y2="14" stroke="currentColor" stroke-width="1.5"/></svg>`,
|
|
34
|
+
strike: `<svg viewBox="0 0 16 16" fill="none" width="14" height="14"><line x1="2" y1="8" x2="14" y2="8" stroke="currentColor" stroke-width="1.5"/><text x="3" y="7" font-family="Arial" font-size="9" fill="currentColor">S</text></svg>`,
|
|
35
|
+
h1: `<svg viewBox="0 0 20 16" fill="none" width="18" height="14"><text x="1" y="13" font-family="Arial" font-size="13" font-weight="bold" fill="currentColor">H1</text></svg>`,
|
|
36
|
+
h2: `<svg viewBox="0 0 20 16" fill="none" width="18" height="14"><text x="1" y="13" font-family="Arial" font-size="13" font-weight="bold" fill="currentColor">H2</text></svg>`,
|
|
37
|
+
h3: `<svg viewBox="0 0 20 16" fill="none" width="18" height="14"><text x="1" y="13" font-family="Arial" font-size="13" font-weight="bold" fill="currentColor">H3</text></svg>`,
|
|
38
|
+
ul: `<svg viewBox="0 0 16 16" fill="none" width="14" height="14" stroke="currentColor" stroke-width="1.2" stroke-linecap="round"><circle cx="3" cy="5" r="1" fill="currentColor" stroke="none"/><line x1="6" y1="5" x2="14" y2="5"/><circle cx="3" cy="9" r="1" fill="currentColor" stroke="none"/><line x1="6" y1="9" x2="14" y2="9"/><circle cx="3" cy="13" r="1" fill="currentColor" stroke="none"/><line x1="6" y1="13" x2="14" y2="13"/></svg>`,
|
|
39
|
+
ol: `<svg viewBox="0 0 16 16" fill="none" width="14" height="14" stroke="currentColor" stroke-width="1.2" stroke-linecap="round"><text x="1" y="6" font-family="Arial" font-size="5" fill="currentColor" stroke="none">1.</text><line x1="6" y1="5" x2="14" y2="5"/><text x="1" y="10" font-family="Arial" font-size="5" fill="currentColor" stroke="none">2.</text><line x1="6" y1="9" x2="14" y2="9"/><text x="1" y="14" font-family="Arial" font-size="5" fill="currentColor" stroke="none">3.</text><line x1="6" y1="13" x2="14" y2="13"/></svg>`,
|
|
40
|
+
blockquote: `<svg viewBox="0 0 16 16" fill="none" width="14" height="14"><text x="1" y="13" font-family="Georgia,serif" font-size="16" fill="currentColor" opacity=".6">"</text></svg>`,
|
|
41
|
+
code: `<svg viewBox="0 0 16 16" fill="none" width="14" height="14" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"><path d="M5 4L1 8l4 4M11 4l4 4-4 4"/></svg>`,
|
|
42
|
+
link: `<svg viewBox="0 0 16 16" fill="none" width="14" height="14" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"><path d="M6.5 9.5a3.5 3.5 0 005 0l2-2a3.5 3.5 0 00-5-5l-1 1"/><path d="M9.5 6.5a3.5 3.5 0 00-5 0l-2 2a3.5 3.5 0 005 5l1-1"/></svg>`,
|
|
43
|
+
'align-left': `<svg viewBox="0 0 16 16" fill="none" width="14" height="14" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"><line x1="2" y1="4" x2="14" y2="4"/><line x1="2" y1="8" x2="10" y2="8"/><line x1="2" y1="12" x2="14" y2="12"/></svg>`,
|
|
44
|
+
'align-center': `<svg viewBox="0 0 16 16" fill="none" width="14" height="14" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"><line x1="2" y1="4" x2="14" y2="4"/><line x1="4" y1="8" x2="12" y2="8"/><line x1="2" y1="12" x2="14" y2="12"/></svg>`,
|
|
45
|
+
'align-right': `<svg viewBox="0 0 16 16" fill="none" width="14" height="14" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"><line x1="2" y1="4" x2="14" y2="4"/><line x1="6" y1="8" x2="14" y2="8"/><line x1="2" y1="12" x2="14" y2="12"/></svg>`,
|
|
46
|
+
clear: `<svg viewBox="0 0 16 16" fill="none" width="14" height="14" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"><path d="M3 3l10 10M13 3L3 13"/></svg>`,
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
export class NcRichText extends Component {
|
|
50
|
+
static useShadowDOM = true;
|
|
51
|
+
|
|
52
|
+
static get observedAttributes() {
|
|
53
|
+
return ['name', 'value', 'placeholder', 'disabled', 'readonly', 'toolbar', 'min-height', 'max-height'];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
private _value = '';
|
|
57
|
+
|
|
58
|
+
constructor() { super(); }
|
|
59
|
+
|
|
60
|
+
private _getToolbarItems(): string[] {
|
|
61
|
+
const raw = this.getAttribute('toolbar');
|
|
62
|
+
if (raw) return raw.split(',').map(s => s.trim());
|
|
63
|
+
return ['bold','italic','underline','strike','|','h1','h2','h3','|','ul','ol','|','blockquote','code','|','link','|','align-left','align-center','align-right','|','clear'];
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
template() {
|
|
67
|
+
if (!this._mounted) {
|
|
68
|
+
this._value = this.getAttribute('value') || '';
|
|
69
|
+
}
|
|
70
|
+
const placeholder = this.getAttribute('placeholder') || 'Start typing...';
|
|
71
|
+
const disabled = this.hasAttribute('disabled');
|
|
72
|
+
const readonly = this.hasAttribute('readonly');
|
|
73
|
+
const minHeight = this.getAttribute('min-height') || '120px';
|
|
74
|
+
const maxHeight = this.getAttribute('max-height') || '400px';
|
|
75
|
+
const items = this._getToolbarItems();
|
|
76
|
+
|
|
77
|
+
const toolbarHtml = items.map(id => {
|
|
78
|
+
if (id === '|') return `<span class="tb-sep"></span>`;
|
|
79
|
+
const icon = ICONS[id] ?? '';
|
|
80
|
+
return `<button class="tb-btn" type="button" data-cmd="${id}" title="${id}" tabindex="-1">${icon}</button>`;
|
|
81
|
+
}).join('');
|
|
82
|
+
|
|
83
|
+
return `
|
|
84
|
+
<style>
|
|
85
|
+
:host { display: block; font-family: var(--nc-font-family); }
|
|
86
|
+
|
|
87
|
+
.editor-wrap {
|
|
88
|
+
border: var(--nc-input-border);
|
|
89
|
+
border-radius: var(--nc-input-radius);
|
|
90
|
+
background: var(--nc-bg);
|
|
91
|
+
overflow: hidden;
|
|
92
|
+
opacity: ${disabled ? '0.5' : '1'};
|
|
93
|
+
pointer-events: ${disabled ? 'none' : 'auto'};
|
|
94
|
+
transition: border-color var(--nc-transition-fast), box-shadow var(--nc-transition-fast);
|
|
95
|
+
}
|
|
96
|
+
.editor-wrap:focus-within { border-color: var(--nc-input-focus-border); box-shadow: 0 0 0 3px rgba(16,185,129,.15); }
|
|
97
|
+
|
|
98
|
+
.toolbar {
|
|
99
|
+
display: flex;
|
|
100
|
+
flex-wrap: wrap;
|
|
101
|
+
align-items: center;
|
|
102
|
+
gap: 2px;
|
|
103
|
+
padding: 6px 8px;
|
|
104
|
+
border-bottom: 1px solid var(--nc-border);
|
|
105
|
+
background: var(--nc-bg-secondary);
|
|
106
|
+
user-select: none;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
.tb-btn {
|
|
110
|
+
display: inline-flex;
|
|
111
|
+
align-items: center;
|
|
112
|
+
justify-content: center;
|
|
113
|
+
background: none;
|
|
114
|
+
border: none;
|
|
115
|
+
cursor: pointer;
|
|
116
|
+
padding: 4px 6px;
|
|
117
|
+
border-radius: var(--nc-radius-sm, 4px);
|
|
118
|
+
color: var(--nc-text);
|
|
119
|
+
transition: background var(--nc-transition-fast), color var(--nc-transition-fast);
|
|
120
|
+
line-height: 1;
|
|
121
|
+
}
|
|
122
|
+
.tb-btn:hover { background: var(--nc-bg-tertiary); }
|
|
123
|
+
.tb-btn.active { background: var(--nc-primary); color: #fff; }
|
|
124
|
+
|
|
125
|
+
.tb-sep {
|
|
126
|
+
width: 1px;
|
|
127
|
+
height: 18px;
|
|
128
|
+
background: var(--nc-border);
|
|
129
|
+
margin: 0 4px;
|
|
130
|
+
flex-shrink: 0;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
.editable {
|
|
134
|
+
padding: var(--nc-spacing-md);
|
|
135
|
+
min-height: ${minHeight};
|
|
136
|
+
max-height: ${maxHeight};
|
|
137
|
+
overflow-y: auto;
|
|
138
|
+
outline: none;
|
|
139
|
+
font-size: var(--nc-font-size-base);
|
|
140
|
+
line-height: var(--nc-line-height-relaxed, 1.7);
|
|
141
|
+
color: var(--nc-text);
|
|
142
|
+
word-break: break-word;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
.editable:empty::before {
|
|
146
|
+
content: attr(data-placeholder);
|
|
147
|
+
color: var(--nc-text-muted);
|
|
148
|
+
pointer-events: none;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/* Content styles */
|
|
152
|
+
.editable h1 { font-size: 1.8em; font-weight: 700; margin: 0.5em 0 0.25em; }
|
|
153
|
+
.editable h2 { font-size: 1.4em; font-weight: 700; margin: 0.5em 0 0.25em; }
|
|
154
|
+
.editable h3 { font-size: 1.1em; font-weight: 700; margin: 0.5em 0 0.25em; }
|
|
155
|
+
.editable ul, .editable ol { padding-left: 1.5em; margin: 0.4em 0; }
|
|
156
|
+
.editable blockquote { border-left: 3px solid var(--nc-primary); padding-left: var(--nc-spacing-md); margin: 0.4em 0; color: var(--nc-text-muted); font-style: italic; }
|
|
157
|
+
.editable code, .editable pre { background: var(--nc-bg-tertiary); border-radius: 4px; font-family: monospace; font-size: 0.9em; padding: 1px 5px; }
|
|
158
|
+
.editable a { color: var(--nc-primary); text-decoration: underline; }
|
|
159
|
+
</style>
|
|
160
|
+
<div class="editor-wrap">
|
|
161
|
+
<div class="toolbar">${toolbarHtml}</div>
|
|
162
|
+
<div
|
|
163
|
+
class="editable"
|
|
164
|
+
contenteditable="${!disabled && !readonly ? 'true' : 'false'}"
|
|
165
|
+
data-placeholder="${placeholder}"
|
|
166
|
+
role="textbox"
|
|
167
|
+
aria-multiline="true"
|
|
168
|
+
aria-label="${placeholder}"
|
|
169
|
+
>${this._value}</div>
|
|
170
|
+
</div>
|
|
171
|
+
<input type="hidden" name="${this.getAttribute('name') || ''}" value="" />
|
|
172
|
+
`;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
onMount() {
|
|
176
|
+
this._bindEvents();
|
|
177
|
+
// Sync hidden input with initial value
|
|
178
|
+
const hidden = this.$<HTMLInputElement>('input[type="hidden"]')!;
|
|
179
|
+
hidden.value = this._value;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
private _exec(cmd: string) {
|
|
183
|
+
const editable = this.$<HTMLElement>('.editable')!;
|
|
184
|
+
editable.focus();
|
|
185
|
+
|
|
186
|
+
switch (cmd) {
|
|
187
|
+
case 'bold': document.execCommand('bold', false); break;
|
|
188
|
+
case 'italic': document.execCommand('italic', false); break;
|
|
189
|
+
case 'underline': document.execCommand('underline', false); break;
|
|
190
|
+
case 'strike': document.execCommand('strikeThrough', false); break;
|
|
191
|
+
case 'h1': document.execCommand('formatBlock', false, 'h1'); break;
|
|
192
|
+
case 'h2': document.execCommand('formatBlock', false, 'h2'); break;
|
|
193
|
+
case 'h3': document.execCommand('formatBlock', false, 'h3'); break;
|
|
194
|
+
case 'ul': document.execCommand('insertUnorderedList', false); break;
|
|
195
|
+
case 'ol': document.execCommand('insertOrderedList', false); break;
|
|
196
|
+
case 'blockquote': document.execCommand('formatBlock', false, 'blockquote'); break;
|
|
197
|
+
case 'code': document.execCommand('formatBlock', false, 'pre'); break;
|
|
198
|
+
case 'align-left': document.execCommand('justifyLeft', false); break;
|
|
199
|
+
case 'align-center': document.execCommand('justifyCenter', false); break;
|
|
200
|
+
case 'align-right': document.execCommand('justifyRight', false); break;
|
|
201
|
+
case 'clear': document.execCommand('removeFormat', false); break;
|
|
202
|
+
case 'link': {
|
|
203
|
+
const url = prompt('Enter URL:');
|
|
204
|
+
if (url) document.execCommand('createLink', false, url);
|
|
205
|
+
break;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
this._onContentChange();
|
|
209
|
+
this._updateToolbarState();
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
private _onContentChange() {
|
|
213
|
+
const editable = this.$<HTMLElement>('.editable')!;
|
|
214
|
+
const html = editable.innerHTML;
|
|
215
|
+
this._value = html;
|
|
216
|
+
const hidden = this.$<HTMLInputElement>('input[type="hidden"]');
|
|
217
|
+
if (hidden) hidden.value = html;
|
|
218
|
+
const name = this.getAttribute('name') || '';
|
|
219
|
+
this.dispatchEvent(new CustomEvent('input', {
|
|
220
|
+
bubbles: true, composed: true,
|
|
221
|
+
detail: { value: html, name }
|
|
222
|
+
}));
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
private _updateToolbarState() {
|
|
226
|
+
this.shadowRoot!.querySelectorAll<HTMLButtonElement>('.tb-btn').forEach(btn => {
|
|
227
|
+
const cmd = btn.dataset.cmd ?? '';
|
|
228
|
+
let active = false;
|
|
229
|
+
switch (cmd) {
|
|
230
|
+
case 'bold': active = document.queryCommandState('bold'); break;
|
|
231
|
+
case 'italic': active = document.queryCommandState('italic'); break;
|
|
232
|
+
case 'underline': active = document.queryCommandState('underline'); break;
|
|
233
|
+
case 'strike': active = document.queryCommandState('strikeThrough'); break;
|
|
234
|
+
}
|
|
235
|
+
btn.classList.toggle('active', active);
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
private _bindEvents() {
|
|
240
|
+
const toolbar = this.$<HTMLElement>('.toolbar')!;
|
|
241
|
+
const editable = this.$<HTMLElement>('.editable')!;
|
|
242
|
+
|
|
243
|
+
toolbar.addEventListener('mousedown', (e) => {
|
|
244
|
+
// Prevent losing selection when clicking toolbar buttons
|
|
245
|
+
e.preventDefault();
|
|
246
|
+
});
|
|
247
|
+
toolbar.addEventListener('click', (e) => {
|
|
248
|
+
const btn = (e.target as HTMLElement).closest<HTMLButtonElement>('[data-cmd]');
|
|
249
|
+
if (btn) this._exec(btn.dataset.cmd!);
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
editable.addEventListener('input', () => this._onContentChange());
|
|
253
|
+
editable.addEventListener('blur', () => {
|
|
254
|
+
this.dispatchEvent(new CustomEvent('change', {
|
|
255
|
+
bubbles: true, composed: true,
|
|
256
|
+
detail: { value: this._value, name: this.getAttribute('name') || '' }
|
|
257
|
+
}));
|
|
258
|
+
});
|
|
259
|
+
editable.addEventListener('keyup', () => this._updateToolbarState());
|
|
260
|
+
editable.addEventListener('mouseup', () => this._updateToolbarState());
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
getValue(): string { return this._value; }
|
|
264
|
+
|
|
265
|
+
setValue(html: string) {
|
|
266
|
+
this._value = html;
|
|
267
|
+
const editable = this.$<HTMLElement>('.editable');
|
|
268
|
+
if (editable) editable.innerHTML = html;
|
|
269
|
+
const hidden = this.$<HTMLInputElement>('input[type="hidden"]');
|
|
270
|
+
if (hidden) hidden.value = html;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
attributeChangedCallback(name: string, oldValue: string, newValue: string) {
|
|
274
|
+
if (oldValue === newValue) return;
|
|
275
|
+
if (name === 'value' && this._mounted) {
|
|
276
|
+
this.setValue(newValue || '');
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
if (this._mounted) { this.render(); this._bindEvents(); }
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
defineComponent('nc-rich-text', NcRichText);
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* NcScrollTop Component — floating "back to top" button
|
|
3
|
+
*
|
|
4
|
+
* Appends itself to document.body so it always sits over all content.
|
|
5
|
+
* Becomes visible after the user scrolls past `threshold` px.
|
|
6
|
+
*
|
|
7
|
+
* Attributes:
|
|
8
|
+
* threshold — scroll distance in px before appearing (default: 300)
|
|
9
|
+
* position — 'bottom-right'(default)|'bottom-left'|'bottom-center'
|
|
10
|
+
* smooth — boolean — use smooth scrolling (default: true)
|
|
11
|
+
* label — accessible aria-label (default: 'Back to top')
|
|
12
|
+
* offset — distance from screen edge in px (default: 24)
|
|
13
|
+
* target — optional CSS selector for the scroll container (default: window)
|
|
14
|
+
*
|
|
15
|
+
* Usage:
|
|
16
|
+
* <nc-scroll-top></nc-scroll-top>
|
|
17
|
+
*/
|
|
18
|
+
import { Component, defineComponent } from '@core/component.js';
|
|
19
|
+
import { addPassiveListener } from '@core/gpu-animation.js';
|
|
20
|
+
import { dom } from '@utils/dom.js';
|
|
21
|
+
|
|
22
|
+
export class NcScrollTop extends Component {
|
|
23
|
+
static useShadowDOM = true;
|
|
24
|
+
|
|
25
|
+
private _visible = false;
|
|
26
|
+
private _removeScroll: (() => void) | null = null;
|
|
27
|
+
private _scrollTarget: HTMLElement | Window = window;
|
|
28
|
+
|
|
29
|
+
template() {
|
|
30
|
+
const pos = this.getAttribute('position') ?? 'bottom-right';
|
|
31
|
+
const offset = parseInt(this.getAttribute('offset') ?? '24', 10);
|
|
32
|
+
const label = this.getAttribute('label') ?? 'Back to top';
|
|
33
|
+
const v = this._visible;
|
|
34
|
+
|
|
35
|
+
const posStyle =
|
|
36
|
+
pos === 'bottom-left' ? `left:${offset}px;right:auto;` :
|
|
37
|
+
pos === 'bottom-center' ? `left:50%;transform:translateX(-50%);` :
|
|
38
|
+
`right:${offset}px;left:auto;`;
|
|
39
|
+
|
|
40
|
+
return `
|
|
41
|
+
<style>
|
|
42
|
+
:host { display: contents; }
|
|
43
|
+
button {
|
|
44
|
+
position: fixed;
|
|
45
|
+
bottom: ${offset}px;
|
|
46
|
+
${posStyle}
|
|
47
|
+
z-index: 900;
|
|
48
|
+
width: 44px;
|
|
49
|
+
height: 44px;
|
|
50
|
+
border-radius: 50%;
|
|
51
|
+
background: var(--nc-primary);
|
|
52
|
+
color: var(--nc-white);
|
|
53
|
+
border: none;
|
|
54
|
+
cursor: pointer;
|
|
55
|
+
display: flex;
|
|
56
|
+
align-items: center;
|
|
57
|
+
justify-content: center;
|
|
58
|
+
box-shadow: var(--nc-shadow-md);
|
|
59
|
+
opacity: ${v ? '1' : '0'};
|
|
60
|
+
visibility: ${v ? 'visible' : 'hidden'};
|
|
61
|
+
transform: ${v ? 'translateY(0) scale(1)' : 'translateY(12px) scale(0.9)'};
|
|
62
|
+
pointer-events: ${v ? 'auto' : 'none'};
|
|
63
|
+
transition:
|
|
64
|
+
opacity var(--nc-transition-base),
|
|
65
|
+
transform var(--nc-transition-base);
|
|
66
|
+
outline: none;
|
|
67
|
+
}
|
|
68
|
+
button:hover { opacity: 0.85; }
|
|
69
|
+
button:active { transform: scale(0.94); }
|
|
70
|
+
button:focus-visible { outline: 2px solid var(--nc-primary); outline-offset: 3px; }
|
|
71
|
+
</style>
|
|
72
|
+
<button type="button" aria-label="${label}" tabindex="${v ? '0' : '-1'}">
|
|
73
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24"
|
|
74
|
+
fill="none" stroke="currentColor" stroke-width="2.5"
|
|
75
|
+
stroke-linecap="round" stroke-linejoin="round">
|
|
76
|
+
<polyline points="18 15 12 9 6 15"/>
|
|
77
|
+
</svg>
|
|
78
|
+
</button>
|
|
79
|
+
`;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
onMount() {
|
|
83
|
+
const threshold = parseInt(this.getAttribute('threshold') ?? '300', 10);
|
|
84
|
+
const targetSelector = this.getAttribute('target');
|
|
85
|
+
this._scrollTarget = targetSelector
|
|
86
|
+
? dom.query<HTMLElement>(targetSelector) ?? window
|
|
87
|
+
: window;
|
|
88
|
+
|
|
89
|
+
const updateVisibility = () => {
|
|
90
|
+
const currentScroll = this._scrollTarget instanceof Window
|
|
91
|
+
? this._scrollTarget.scrollY
|
|
92
|
+
: this._scrollTarget.scrollTop;
|
|
93
|
+
const shouldShow = currentScroll > threshold;
|
|
94
|
+
if (shouldShow !== this._visible) {
|
|
95
|
+
this._visible = shouldShow;
|
|
96
|
+
this.render();
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
this._removeScroll = addPassiveListener(this._scrollTarget, 'scroll', updateVisibility);
|
|
101
|
+
updateVisibility();
|
|
102
|
+
|
|
103
|
+
this.shadowRoot!.addEventListener('click', () => this._scrollTop());
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
private _scrollTop() {
|
|
107
|
+
const smooth = this.getAttribute('smooth') !== 'false';
|
|
108
|
+
this._scrollTarget.scrollTo({ top: 0, behavior: smooth ? 'smooth' : 'auto' });
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
onUnmount() {
|
|
112
|
+
this._removeScroll?.();
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
defineComponent('nc-scroll-top', NcScrollTop);
|