nativecorejs 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 +22 -0
- package/dist/components/builtinRegistry.d.ts +2 -0
- package/dist/components/builtinRegistry.js +72 -0
- package/dist/components/index.d.ts +59 -0
- package/dist/components/index.js +59 -0
- package/dist/components/loading-spinner.d.ts +5 -0
- package/dist/components/loading-spinner.js +48 -0
- package/dist/components/nc-a.d.ts +45 -0
- package/dist/components/nc-a.js +290 -0
- package/dist/components/nc-accordion.d.ts +36 -0
- package/dist/components/nc-accordion.js +186 -0
- package/dist/components/nc-alert.d.ts +11 -0
- package/dist/components/nc-alert.js +127 -0
- package/dist/components/nc-animation.d.ts +117 -0
- package/dist/components/nc-animation.js +1053 -0
- package/dist/components/nc-autocomplete.d.ts +41 -0
- package/dist/components/nc-autocomplete.js +275 -0
- package/dist/components/nc-avatar-group.d.ts +7 -0
- package/dist/components/nc-avatar-group.js +85 -0
- package/dist/components/nc-avatar.d.ts +9 -0
- package/dist/components/nc-avatar.js +127 -0
- package/dist/components/nc-badge.d.ts +7 -0
- package/dist/components/nc-badge.js +63 -0
- package/dist/components/nc-bottom-nav.d.ts +53 -0
- package/dist/components/nc-bottom-nav.js +198 -0
- package/dist/components/nc-breadcrumb.d.ts +10 -0
- package/dist/components/nc-breadcrumb.js +71 -0
- package/dist/components/nc-button.d.ts +38 -0
- package/dist/components/nc-button.js +293 -0
- package/dist/components/nc-card.d.ts +11 -0
- package/dist/components/nc-card.js +74 -0
- package/dist/components/nc-checkbox.d.ts +16 -0
- package/dist/components/nc-checkbox.js +194 -0
- package/dist/components/nc-chip.d.ts +8 -0
- package/dist/components/nc-chip.js +89 -0
- package/dist/components/nc-code.d.ts +37 -0
- package/dist/components/nc-code.js +315 -0
- package/dist/components/nc-collapsible.d.ts +33 -0
- package/dist/components/nc-collapsible.js +148 -0
- package/dist/components/nc-color-picker.d.ts +33 -0
- package/dist/components/nc-color-picker.js +265 -0
- package/dist/components/nc-copy-button.d.ts +10 -0
- package/dist/components/nc-copy-button.js +94 -0
- package/dist/components/nc-date-picker.d.ts +41 -0
- package/dist/components/nc-date-picker.js +443 -0
- package/dist/components/nc-div.d.ts +53 -0
- package/dist/components/nc-div.js +270 -0
- package/dist/components/nc-divider.d.ts +7 -0
- package/dist/components/nc-divider.js +57 -0
- package/dist/components/nc-drawer.d.ts +40 -0
- package/dist/components/nc-drawer.js +217 -0
- package/dist/components/nc-dropdown.d.ts +41 -0
- package/dist/components/nc-dropdown.js +170 -0
- package/dist/components/nc-empty-state.d.ts +5 -0
- package/dist/components/nc-empty-state.js +76 -0
- package/dist/components/nc-file-upload.d.ts +40 -0
- package/dist/components/nc-file-upload.js +336 -0
- package/dist/components/nc-form.d.ts +70 -0
- package/dist/components/nc-form.js +273 -0
- package/dist/components/nc-image.d.ts +10 -0
- package/dist/components/nc-image.js +139 -0
- package/dist/components/nc-input.d.ts +25 -0
- package/dist/components/nc-input.js +302 -0
- package/dist/components/nc-kbd.d.ts +5 -0
- package/dist/components/nc-kbd.js +34 -0
- package/dist/components/nc-menu-item.d.ts +43 -0
- package/dist/components/nc-menu-item.js +182 -0
- package/dist/components/nc-menu.d.ts +76 -0
- package/dist/components/nc-menu.js +360 -0
- package/dist/components/nc-modal.d.ts +51 -0
- package/dist/components/nc-modal.js +231 -0
- package/dist/components/nc-nav-item.d.ts +35 -0
- package/dist/components/nc-nav-item.js +142 -0
- package/dist/components/nc-number-input.d.ts +22 -0
- package/dist/components/nc-number-input.js +270 -0
- package/dist/components/nc-otp-input.d.ts +41 -0
- package/dist/components/nc-otp-input.js +227 -0
- package/dist/components/nc-pagination.d.ts +28 -0
- package/dist/components/nc-pagination.js +171 -0
- package/dist/components/nc-popover.d.ts +58 -0
- package/dist/components/nc-popover.js +301 -0
- package/dist/components/nc-progress-circular.d.ts +7 -0
- package/dist/components/nc-progress-circular.js +67 -0
- package/dist/components/nc-progress.d.ts +7 -0
- package/dist/components/nc-progress.js +109 -0
- package/dist/components/nc-radio.d.ts +13 -0
- package/dist/components/nc-radio.js +169 -0
- package/dist/components/nc-rating.d.ts +19 -0
- package/dist/components/nc-rating.js +187 -0
- package/dist/components/nc-rich-text.d.ts +43 -0
- package/dist/components/nc-rich-text.js +310 -0
- package/dist/components/nc-scroll-top.d.ts +28 -0
- package/dist/components/nc-scroll-top.js +103 -0
- package/dist/components/nc-select.d.ts +51 -0
- package/dist/components/nc-select.js +425 -0
- package/dist/components/nc-skeleton.d.ts +7 -0
- package/dist/components/nc-skeleton.js +90 -0
- package/dist/components/nc-slider.d.ts +41 -0
- package/dist/components/nc-slider.js +268 -0
- package/dist/components/nc-snackbar.d.ts +51 -0
- package/dist/components/nc-snackbar.js +200 -0
- package/dist/components/nc-splash.d.ts +25 -0
- package/dist/components/nc-splash.js +296 -0
- package/dist/components/nc-stepper.d.ts +50 -0
- package/dist/components/nc-stepper.js +236 -0
- package/dist/components/nc-switch.d.ts +14 -0
- package/dist/components/nc-switch.js +194 -0
- package/dist/components/nc-tab-item.d.ts +39 -0
- package/dist/components/nc-tab-item.js +127 -0
- package/dist/components/nc-table.d.ts +44 -0
- package/dist/components/nc-table.js +265 -0
- package/dist/components/nc-tabs.d.ts +79 -0
- package/dist/components/nc-tabs.js +519 -0
- package/dist/components/nc-tag-input.d.ts +49 -0
- package/dist/components/nc-tag-input.js +268 -0
- package/dist/components/nc-textarea.d.ts +15 -0
- package/dist/components/nc-textarea.js +164 -0
- package/dist/components/nc-time-picker.d.ts +51 -0
- package/dist/components/nc-time-picker.js +452 -0
- package/dist/components/nc-timeline.d.ts +53 -0
- package/dist/components/nc-timeline.js +171 -0
- package/dist/components/nc-tooltip.d.ts +27 -0
- package/dist/components/nc-tooltip.js +135 -0
- package/dist/core/component.d.ts +33 -0
- package/dist/core/component.js +208 -0
- package/dist/core/gpu-animation.d.ts +141 -0
- package/dist/core/gpu-animation.js +474 -0
- package/dist/core/lazyComponents.d.ts +13 -0
- package/dist/core/lazyComponents.js +73 -0
- package/dist/core/router.d.ts +55 -0
- package/dist/core/router.js +424 -0
- package/dist/core/state.d.ts +18 -0
- package/dist/core/state.js +153 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +11 -0
- package/dist/utils/cacheBuster.d.ts +9 -0
- package/dist/utils/cacheBuster.js +12 -0
- package/dist/utils/dom.d.ts +16 -0
- package/dist/utils/dom.js +70 -0
- package/dist/utils/events.d.ts +20 -0
- package/dist/utils/events.js +80 -0
- package/dist/utils/templates.d.ts +2 -0
- package/dist/utils/templates.js +2 -0
- package/package.json +53 -0
- package/src/styles/base.css +40 -0
|
@@ -0,0 +1,103 @@
|
|
|
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
|
+
export class NcScrollTop extends Component {
|
|
22
|
+
static useShadowDOM = true;
|
|
23
|
+
_visible = false;
|
|
24
|
+
_removeScroll = null;
|
|
25
|
+
_scrollTarget = window;
|
|
26
|
+
template() {
|
|
27
|
+
const pos = this.getAttribute('position') ?? 'bottom-right';
|
|
28
|
+
const offset = parseInt(this.getAttribute('offset') ?? '24', 10);
|
|
29
|
+
const label = this.getAttribute('label') ?? 'Back to top';
|
|
30
|
+
const v = this._visible;
|
|
31
|
+
const posStyle = pos === 'bottom-left' ? `left:${offset}px;right:auto;` :
|
|
32
|
+
pos === 'bottom-center' ? `left:50%;transform:translateX(-50%);` :
|
|
33
|
+
`right:${offset}px;left:auto;`;
|
|
34
|
+
return `
|
|
35
|
+
<style>
|
|
36
|
+
:host { display: contents; }
|
|
37
|
+
button {
|
|
38
|
+
position: fixed;
|
|
39
|
+
bottom: ${offset}px;
|
|
40
|
+
${posStyle}
|
|
41
|
+
z-index: 900;
|
|
42
|
+
width: 44px;
|
|
43
|
+
height: 44px;
|
|
44
|
+
border-radius: 50%;
|
|
45
|
+
background: var(--nc-primary);
|
|
46
|
+
color: var(--nc-white);
|
|
47
|
+
border: none;
|
|
48
|
+
cursor: pointer;
|
|
49
|
+
display: flex;
|
|
50
|
+
align-items: center;
|
|
51
|
+
justify-content: center;
|
|
52
|
+
box-shadow: var(--nc-shadow-md);
|
|
53
|
+
opacity: ${v ? '1' : '0'};
|
|
54
|
+
visibility: ${v ? 'visible' : 'hidden'};
|
|
55
|
+
transform: ${v ? 'translateY(0) scale(1)' : 'translateY(12px) scale(0.9)'};
|
|
56
|
+
pointer-events: ${v ? 'auto' : 'none'};
|
|
57
|
+
transition:
|
|
58
|
+
opacity var(--nc-transition-base),
|
|
59
|
+
transform var(--nc-transition-base);
|
|
60
|
+
outline: none;
|
|
61
|
+
}
|
|
62
|
+
button:hover { opacity: 0.85; }
|
|
63
|
+
button:active { transform: scale(0.94); }
|
|
64
|
+
button:focus-visible { outline: 2px solid var(--nc-primary); outline-offset: 3px; }
|
|
65
|
+
</style>
|
|
66
|
+
<button type="button" aria-label="${label}" tabindex="${v ? '0' : '-1'}">
|
|
67
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24"
|
|
68
|
+
fill="none" stroke="currentColor" stroke-width="2.5"
|
|
69
|
+
stroke-linecap="round" stroke-linejoin="round">
|
|
70
|
+
<polyline points="18 15 12 9 6 15"/>
|
|
71
|
+
</svg>
|
|
72
|
+
</button>
|
|
73
|
+
`;
|
|
74
|
+
}
|
|
75
|
+
onMount() {
|
|
76
|
+
const threshold = parseInt(this.getAttribute('threshold') ?? '300', 10);
|
|
77
|
+
const targetSelector = this.getAttribute('target');
|
|
78
|
+
this._scrollTarget = targetSelector
|
|
79
|
+
? dom.query(targetSelector) ?? window
|
|
80
|
+
: window;
|
|
81
|
+
const updateVisibility = () => {
|
|
82
|
+
const currentScroll = this._scrollTarget instanceof Window
|
|
83
|
+
? this._scrollTarget.scrollY
|
|
84
|
+
: this._scrollTarget.scrollTop;
|
|
85
|
+
const shouldShow = currentScroll > threshold;
|
|
86
|
+
if (shouldShow !== this._visible) {
|
|
87
|
+
this._visible = shouldShow;
|
|
88
|
+
this.render();
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
this._removeScroll = addPassiveListener(this._scrollTarget, 'scroll', updateVisibility);
|
|
92
|
+
updateVisibility();
|
|
93
|
+
this.shadowRoot.addEventListener('click', () => this._scrollTop());
|
|
94
|
+
}
|
|
95
|
+
_scrollTop() {
|
|
96
|
+
const smooth = this.getAttribute('smooth') !== 'false';
|
|
97
|
+
this._scrollTarget.scrollTo({ top: 0, behavior: smooth ? 'smooth' : 'auto' });
|
|
98
|
+
}
|
|
99
|
+
onUnmount() {
|
|
100
|
+
this._removeScroll?.();
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
defineComponent('nc-scroll-top', NcScrollTop);
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* NcSelect Component
|
|
3
|
+
*
|
|
4
|
+
* NativeCore Framework Core Component
|
|
5
|
+
*
|
|
6
|
+
* Options are provided as a JSON array via the `options` attribute or by
|
|
7
|
+
* populating child `<option>` elements before the component mounts.
|
|
8
|
+
*
|
|
9
|
+
* Attributes:
|
|
10
|
+
* - options: JSON string - array of { value, label, disabled? }
|
|
11
|
+
* - value: string - currently selected value
|
|
12
|
+
* - placeholder: string - shown when no value selected (default: 'Select...')
|
|
13
|
+
* - name: string - form field name
|
|
14
|
+
* - disabled: boolean - disabled state
|
|
15
|
+
* - size: 'sm' | 'md' | 'lg' (default: 'md')
|
|
16
|
+
* - variant: 'default' | 'filled' (default: 'default')
|
|
17
|
+
* - searchable: boolean - adds a live filter input inside the dropdown
|
|
18
|
+
*
|
|
19
|
+
* Events:
|
|
20
|
+
* - change: CustomEvent<{ value: string; label: string; name: string }>
|
|
21
|
+
*
|
|
22
|
+
* Usage:
|
|
23
|
+
* <nc-select
|
|
24
|
+
* name="country"
|
|
25
|
+
* placeholder="Pick a country"
|
|
26
|
+
* options='[{"value":"us","label":"United States"},{"value":"ca","label":"Canada"}]'>
|
|
27
|
+
* </nc-select>
|
|
28
|
+
*/
|
|
29
|
+
import { Component } from '../core/component.js';
|
|
30
|
+
export declare class NcSelect extends Component {
|
|
31
|
+
static useShadowDOM: boolean;
|
|
32
|
+
static attributeOptions: {
|
|
33
|
+
variant: string[];
|
|
34
|
+
size: string[];
|
|
35
|
+
};
|
|
36
|
+
static get observedAttributes(): string[];
|
|
37
|
+
private _open;
|
|
38
|
+
private _filterText;
|
|
39
|
+
constructor();
|
|
40
|
+
private _getOptions;
|
|
41
|
+
private _getSelectedLabel;
|
|
42
|
+
template(): string;
|
|
43
|
+
onMount(): void;
|
|
44
|
+
private _onOutsideClick;
|
|
45
|
+
private _setOpen;
|
|
46
|
+
private _rerenderDropdown;
|
|
47
|
+
private _select;
|
|
48
|
+
private _navigateOptions;
|
|
49
|
+
onUnmount(): void;
|
|
50
|
+
attributeChangedCallback(name: string, oldValue: string, newValue: string): void;
|
|
51
|
+
}
|
|
@@ -0,0 +1,425 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* NcSelect Component
|
|
3
|
+
*
|
|
4
|
+
* NativeCore Framework Core Component
|
|
5
|
+
*
|
|
6
|
+
* Options are provided as a JSON array via the `options` attribute or by
|
|
7
|
+
* populating child `<option>` elements before the component mounts.
|
|
8
|
+
*
|
|
9
|
+
* Attributes:
|
|
10
|
+
* - options: JSON string - array of { value, label, disabled? }
|
|
11
|
+
* - value: string - currently selected value
|
|
12
|
+
* - placeholder: string - shown when no value selected (default: 'Select...')
|
|
13
|
+
* - name: string - form field name
|
|
14
|
+
* - disabled: boolean - disabled state
|
|
15
|
+
* - size: 'sm' | 'md' | 'lg' (default: 'md')
|
|
16
|
+
* - variant: 'default' | 'filled' (default: 'default')
|
|
17
|
+
* - searchable: boolean - adds a live filter input inside the dropdown
|
|
18
|
+
*
|
|
19
|
+
* Events:
|
|
20
|
+
* - change: CustomEvent<{ value: string; label: string; name: string }>
|
|
21
|
+
*
|
|
22
|
+
* Usage:
|
|
23
|
+
* <nc-select
|
|
24
|
+
* name="country"
|
|
25
|
+
* placeholder="Pick a country"
|
|
26
|
+
* options='[{"value":"us","label":"United States"},{"value":"ca","label":"Canada"}]'>
|
|
27
|
+
* </nc-select>
|
|
28
|
+
*/
|
|
29
|
+
import { Component, defineComponent } from '../core/component.js';
|
|
30
|
+
export class NcSelect extends Component {
|
|
31
|
+
static useShadowDOM = true;
|
|
32
|
+
static attributeOptions = {
|
|
33
|
+
variant: ['default', 'filled'],
|
|
34
|
+
size: ['sm', 'md', 'lg']
|
|
35
|
+
};
|
|
36
|
+
static get observedAttributes() {
|
|
37
|
+
return ['options', 'value', 'placeholder', 'name', 'disabled', 'size', 'variant', 'searchable'];
|
|
38
|
+
}
|
|
39
|
+
_open = false;
|
|
40
|
+
_filterText = '';
|
|
41
|
+
constructor() {
|
|
42
|
+
super();
|
|
43
|
+
}
|
|
44
|
+
_getOptions() {
|
|
45
|
+
try {
|
|
46
|
+
const raw = this.getAttribute('options');
|
|
47
|
+
if (raw)
|
|
48
|
+
return JSON.parse(raw);
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
// fall through
|
|
52
|
+
}
|
|
53
|
+
return [];
|
|
54
|
+
}
|
|
55
|
+
_getSelectedLabel() {
|
|
56
|
+
const value = this.getAttribute('value') || '';
|
|
57
|
+
if (!value)
|
|
58
|
+
return '';
|
|
59
|
+
const opt = this._getOptions().find(o => o.value === value);
|
|
60
|
+
return opt?.label ?? value;
|
|
61
|
+
}
|
|
62
|
+
template() {
|
|
63
|
+
const value = this.getAttribute('value') || '';
|
|
64
|
+
const placeholder = this.getAttribute('placeholder') || 'Select...';
|
|
65
|
+
const disabled = this.hasAttribute('disabled');
|
|
66
|
+
const searchable = this.hasAttribute('searchable');
|
|
67
|
+
const selectedLabel = this._getSelectedLabel() || placeholder;
|
|
68
|
+
const hasValue = !!value;
|
|
69
|
+
const options = this._getOptions();
|
|
70
|
+
const filtered = this._filterText
|
|
71
|
+
? options.filter(o => o.label.toLowerCase().includes(this._filterText.toLowerCase()))
|
|
72
|
+
: options;
|
|
73
|
+
const optionItems = filtered.map(o => `
|
|
74
|
+
<div class="option${o.value === value ? ' option--selected' : ''}${o.disabled ? ' option--disabled' : ''}"
|
|
75
|
+
data-value="${o.value}"
|
|
76
|
+
role="option"
|
|
77
|
+
aria-selected="${o.value === value}"
|
|
78
|
+
aria-disabled="${o.disabled ? 'true' : 'false'}">
|
|
79
|
+
${o.label}
|
|
80
|
+
${o.value === value ? `
|
|
81
|
+
<svg class="option__check" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 12 12" fill="none" width="12" height="12">
|
|
82
|
+
<path d="M2 6l3 3 5-5" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
|
83
|
+
</svg>` : ''}
|
|
84
|
+
</div>
|
|
85
|
+
`).join('');
|
|
86
|
+
return `
|
|
87
|
+
<style>
|
|
88
|
+
:host {
|
|
89
|
+
display: inline-block;
|
|
90
|
+
position: relative;
|
|
91
|
+
font-family: var(--nc-font-family);
|
|
92
|
+
width: 100%;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
.select-trigger {
|
|
96
|
+
display: flex;
|
|
97
|
+
align-items: center;
|
|
98
|
+
justify-content: space-between;
|
|
99
|
+
width: 100%;
|
|
100
|
+
box-sizing: border-box;
|
|
101
|
+
padding: var(--nc-spacing-sm) var(--nc-spacing-md);
|
|
102
|
+
background: var(--nc-bg);
|
|
103
|
+
border: var(--nc-input-border);
|
|
104
|
+
border-radius: var(--nc-input-radius);
|
|
105
|
+
cursor: ${disabled ? 'not-allowed' : 'pointer'};
|
|
106
|
+
color: ${hasValue ? 'var(--nc-text)' : 'var(--nc-text-muted)'};
|
|
107
|
+
font-size: var(--nc-font-size-base);
|
|
108
|
+
transition: border-color var(--nc-transition-fast), box-shadow var(--nc-transition-fast);
|
|
109
|
+
opacity: ${disabled ? '0.5' : '1'};
|
|
110
|
+
user-select: none;
|
|
111
|
+
gap: var(--nc-spacing-sm);
|
|
112
|
+
min-height: 40px;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/* Size variants */
|
|
116
|
+
:host([size="sm"]) .select-trigger {
|
|
117
|
+
padding: var(--nc-spacing-xs) var(--nc-spacing-sm);
|
|
118
|
+
font-size: var(--nc-font-size-sm);
|
|
119
|
+
min-height: 32px;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
:host([size="lg"]) .select-trigger {
|
|
123
|
+
padding: var(--nc-spacing-md) var(--nc-spacing-lg);
|
|
124
|
+
font-size: var(--nc-font-size-lg);
|
|
125
|
+
min-height: 48px;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/* Filled variant */
|
|
129
|
+
:host([variant="filled"]) .select-trigger {
|
|
130
|
+
background: var(--nc-bg-tertiary);
|
|
131
|
+
border-color: transparent;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
:host([variant="filled"]) .select-trigger:hover:not([disabled]) {
|
|
135
|
+
background: var(--nc-bg-secondary);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
.select-trigger:hover {
|
|
139
|
+
border-color: var(--nc-input-focus-border);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
.select-trigger.open {
|
|
143
|
+
border-color: var(--nc-input-focus-border);
|
|
144
|
+
box-shadow: 0 0 0 3px rgba(16, 185, 129, 0.15);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
.trigger-label {
|
|
148
|
+
flex: 1;
|
|
149
|
+
overflow: hidden;
|
|
150
|
+
white-space: nowrap;
|
|
151
|
+
text-overflow: ellipsis;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
.chevron {
|
|
155
|
+
flex-shrink: 0;
|
|
156
|
+
transition: transform var(--nc-transition-fast);
|
|
157
|
+
color: var(--nc-text-muted);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
.chevron.open {
|
|
161
|
+
transform: rotate(180deg);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/* Dropdown */
|
|
165
|
+
.dropdown {
|
|
166
|
+
display: none;
|
|
167
|
+
position: absolute;
|
|
168
|
+
top: calc(100% + 4px);
|
|
169
|
+
left: 0;
|
|
170
|
+
right: 0;
|
|
171
|
+
background: var(--nc-bg);
|
|
172
|
+
border: var(--nc-input-border);
|
|
173
|
+
border-radius: var(--nc-radius-md);
|
|
174
|
+
box-shadow: var(--nc-shadow-lg);
|
|
175
|
+
z-index: var(--nc-z-dropdown);
|
|
176
|
+
overflow: hidden;
|
|
177
|
+
max-height: 240px;
|
|
178
|
+
flex-direction: column;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
.dropdown.open {
|
|
182
|
+
display: flex;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
.search-wrap {
|
|
186
|
+
padding: var(--nc-spacing-xs) var(--nc-spacing-sm);
|
|
187
|
+
border-bottom: 1px solid var(--nc-border);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
.search-input {
|
|
191
|
+
width: 100%;
|
|
192
|
+
box-sizing: border-box;
|
|
193
|
+
border: var(--nc-input-border);
|
|
194
|
+
border-radius: var(--nc-radius-sm);
|
|
195
|
+
padding: var(--nc-spacing-xs) var(--nc-spacing-sm);
|
|
196
|
+
font-size: var(--nc-font-size-sm);
|
|
197
|
+
font-family: var(--nc-font-family);
|
|
198
|
+
color: var(--nc-text);
|
|
199
|
+
background: var(--nc-bg-secondary);
|
|
200
|
+
outline: none;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
.search-input:focus {
|
|
204
|
+
border-color: var(--nc-input-focus-border);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
.options-list {
|
|
208
|
+
overflow-y: auto;
|
|
209
|
+
flex: 1;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
.option {
|
|
213
|
+
display: flex;
|
|
214
|
+
align-items: center;
|
|
215
|
+
justify-content: space-between;
|
|
216
|
+
padding: var(--nc-spacing-sm) var(--nc-spacing-md);
|
|
217
|
+
cursor: pointer;
|
|
218
|
+
font-size: var(--nc-font-size-base);
|
|
219
|
+
color: var(--nc-text);
|
|
220
|
+
transition: background var(--nc-transition-fast);
|
|
221
|
+
gap: var(--nc-spacing-sm);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
.option:hover:not(.option--disabled) {
|
|
225
|
+
background: var(--nc-bg-secondary);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
.option--selected {
|
|
229
|
+
color: var(--nc-primary);
|
|
230
|
+
font-weight: var(--nc-font-weight-medium);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
.option--disabled {
|
|
234
|
+
opacity: 0.4;
|
|
235
|
+
cursor: not-allowed;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
.option__check {
|
|
239
|
+
flex-shrink: 0;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
.empty {
|
|
243
|
+
padding: var(--nc-spacing-md);
|
|
244
|
+
text-align: center;
|
|
245
|
+
color: var(--nc-text-muted);
|
|
246
|
+
font-size: var(--nc-font-size-sm);
|
|
247
|
+
}
|
|
248
|
+
</style>
|
|
249
|
+
|
|
250
|
+
<input type="hidden"
|
|
251
|
+
name="${this.getAttribute('name') || ''}"
|
|
252
|
+
value="${value}"
|
|
253
|
+
/>
|
|
254
|
+
|
|
255
|
+
<div class="select-trigger${this._open ? ' open' : ''}" role="combobox" aria-expanded="${this._open}" aria-haspopup="listbox">
|
|
256
|
+
<span class="trigger-label">${selectedLabel}</span>
|
|
257
|
+
<svg class="chevron${this._open ? ' open' : ''}" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="none" width="16" height="16">
|
|
258
|
+
<path d="M4 6l4 4 4-4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
|
259
|
+
</svg>
|
|
260
|
+
</div>
|
|
261
|
+
|
|
262
|
+
<div class="dropdown${this._open ? ' open' : ''}" role="listbox">
|
|
263
|
+
${searchable ? `
|
|
264
|
+
<div class="search-wrap">
|
|
265
|
+
<input class="search-input" type="text" placeholder="Search..." value="${this._filterText}" autocomplete="off" />
|
|
266
|
+
</div>` : ''}
|
|
267
|
+
<div class="options-list">
|
|
268
|
+
${optionItems || `<div class="empty">No options</div>`}
|
|
269
|
+
</div>
|
|
270
|
+
</div>
|
|
271
|
+
`;
|
|
272
|
+
}
|
|
273
|
+
onMount() {
|
|
274
|
+
if (!this.hasAttribute('tabindex')) {
|
|
275
|
+
this.setAttribute('tabindex', '0');
|
|
276
|
+
}
|
|
277
|
+
const sr = this.shadowRoot;
|
|
278
|
+
// Single click listener - never re-added
|
|
279
|
+
sr.addEventListener('click', (e) => {
|
|
280
|
+
if (this.hasAttribute('disabled'))
|
|
281
|
+
return;
|
|
282
|
+
const target = e.target;
|
|
283
|
+
const option = target.closest('.option');
|
|
284
|
+
if (option) {
|
|
285
|
+
if (option.classList.contains('option--disabled'))
|
|
286
|
+
return;
|
|
287
|
+
this._select(option.dataset.value ?? '');
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
if (target.closest('.select-trigger')) {
|
|
291
|
+
this._setOpen(!this._open);
|
|
292
|
+
}
|
|
293
|
+
});
|
|
294
|
+
// Search filter
|
|
295
|
+
sr.addEventListener('input', (e) => {
|
|
296
|
+
const input = e.target;
|
|
297
|
+
if (input.classList.contains('search-input')) {
|
|
298
|
+
this._filterText = input.value;
|
|
299
|
+
this._rerenderDropdown();
|
|
300
|
+
}
|
|
301
|
+
});
|
|
302
|
+
// Outside click - registered once, cleaned up in onUnmount
|
|
303
|
+
document.addEventListener('click', this._onOutsideClick);
|
|
304
|
+
// Keyboard
|
|
305
|
+
this.addEventListener('keydown', (e) => {
|
|
306
|
+
if (e.key === 'Escape') {
|
|
307
|
+
this._setOpen(false);
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
311
|
+
e.preventDefault();
|
|
312
|
+
this._setOpen(!this._open);
|
|
313
|
+
}
|
|
314
|
+
if (e.key === 'ArrowDown' || e.key === 'ArrowUp') {
|
|
315
|
+
e.preventDefault();
|
|
316
|
+
this._navigateOptions(e.key === 'ArrowDown' ? 1 : -1);
|
|
317
|
+
}
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
_onOutsideClick = (e) => {
|
|
321
|
+
if (!this.contains(e.target) && !this.shadowRoot.contains(e.target)) {
|
|
322
|
+
this._setOpen(false);
|
|
323
|
+
}
|
|
324
|
+
};
|
|
325
|
+
_setOpen(open) {
|
|
326
|
+
this._open = open;
|
|
327
|
+
if (!open)
|
|
328
|
+
this._filterText = '';
|
|
329
|
+
const sr = this.shadowRoot;
|
|
330
|
+
const trigger = sr.querySelector('.select-trigger');
|
|
331
|
+
const chevron = sr.querySelector('.chevron');
|
|
332
|
+
const dropdown = sr.querySelector('.dropdown');
|
|
333
|
+
if (trigger) {
|
|
334
|
+
trigger.classList.toggle('open', open);
|
|
335
|
+
trigger.setAttribute('aria-expanded', String(open));
|
|
336
|
+
}
|
|
337
|
+
if (chevron)
|
|
338
|
+
chevron.classList.toggle('open', open);
|
|
339
|
+
if (dropdown) {
|
|
340
|
+
dropdown.classList.toggle('open', open);
|
|
341
|
+
if (!open) {
|
|
342
|
+
// Clear search when closing
|
|
343
|
+
const searchInput = dropdown.querySelector('.search-input');
|
|
344
|
+
if (searchInput)
|
|
345
|
+
searchInput.value = '';
|
|
346
|
+
this._rerenderDropdown();
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
if (open) {
|
|
350
|
+
const search = sr.querySelector('.search-input');
|
|
351
|
+
if (search)
|
|
352
|
+
search.focus();
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
_rerenderDropdown() {
|
|
356
|
+
const sr = this.shadowRoot;
|
|
357
|
+
const list = sr.querySelector('.options-list');
|
|
358
|
+
if (!list)
|
|
359
|
+
return;
|
|
360
|
+
const value = this.getAttribute('value') || '';
|
|
361
|
+
const options = this._getOptions();
|
|
362
|
+
const filtered = this._filterText
|
|
363
|
+
? options.filter(o => o.label.toLowerCase().includes(this._filterText.toLowerCase()))
|
|
364
|
+
: options;
|
|
365
|
+
list.innerHTML = filtered.length ? filtered.map(o => `
|
|
366
|
+
<div class="option${o.value === value ? ' option--selected' : ''}${o.disabled ? ' option--disabled' : ''}"
|
|
367
|
+
data-value="${o.value}"
|
|
368
|
+
role="option"
|
|
369
|
+
aria-selected="${o.value === value}"
|
|
370
|
+
aria-disabled="${o.disabled ? 'true' : 'false'}">
|
|
371
|
+
${o.label}
|
|
372
|
+
${o.value === value ? `
|
|
373
|
+
<svg class="option__check" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 12 12" fill="none" width="12" height="12">
|
|
374
|
+
<path d="M2 6l3 3 5-5" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
|
375
|
+
</svg>` : ''}
|
|
376
|
+
</div>
|
|
377
|
+
`).join('') : '<div class="empty">No results</div>';
|
|
378
|
+
}
|
|
379
|
+
_select(value) {
|
|
380
|
+
const opts = this._getOptions();
|
|
381
|
+
const opt = opts.find(o => o.value === value);
|
|
382
|
+
if (!opt)
|
|
383
|
+
return;
|
|
384
|
+
this._open = false;
|
|
385
|
+
this._filterText = '';
|
|
386
|
+
// Update trigger label and hidden input directly
|
|
387
|
+
const sr = this.shadowRoot;
|
|
388
|
+
const label = sr.querySelector('.trigger-label');
|
|
389
|
+
if (label)
|
|
390
|
+
label.textContent = opt.label;
|
|
391
|
+
const hidden = sr.querySelector('input[type="hidden"]');
|
|
392
|
+
if (hidden)
|
|
393
|
+
hidden.value = value;
|
|
394
|
+
// Close the dropdown
|
|
395
|
+
this._setOpen(false);
|
|
396
|
+
// Re-render option list to show the new checkmark
|
|
397
|
+
this.setAttribute('value', value);
|
|
398
|
+
this.dispatchEvent(new CustomEvent('change', {
|
|
399
|
+
bubbles: true,
|
|
400
|
+
composed: true,
|
|
401
|
+
detail: {
|
|
402
|
+
value,
|
|
403
|
+
label: opt.label,
|
|
404
|
+
name: this.getAttribute('name') || ''
|
|
405
|
+
}
|
|
406
|
+
}));
|
|
407
|
+
}
|
|
408
|
+
_navigateOptions(direction) {
|
|
409
|
+
const opts = this._getOptions().filter(o => !o.disabled);
|
|
410
|
+
const current = this.getAttribute('value') || '';
|
|
411
|
+
const idx = opts.findIndex(o => o.value === current);
|
|
412
|
+
const next = opts[Math.max(0, Math.min(opts.length - 1, idx + direction))];
|
|
413
|
+
if (next)
|
|
414
|
+
this._select(next.value);
|
|
415
|
+
}
|
|
416
|
+
onUnmount() {
|
|
417
|
+
document.removeEventListener('click', this._onOutsideClick);
|
|
418
|
+
}
|
|
419
|
+
attributeChangedCallback(name, oldValue, newValue) {
|
|
420
|
+
if (oldValue !== newValue && this._mounted) {
|
|
421
|
+
this.render();
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
defineComponent('nc-select', NcSelect);
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { Component } from '../core/component.js';
|
|
2
|
+
export declare class NcSkeleton extends Component {
|
|
3
|
+
static useShadowDOM: boolean;
|
|
4
|
+
static get observedAttributes(): string[];
|
|
5
|
+
template(): string;
|
|
6
|
+
attributeChangedCallback(name: string, oldValue: string, newValue: string): void;
|
|
7
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { Component, defineComponent } from '../core/component.js';
|
|
2
|
+
export class NcSkeleton extends Component {
|
|
3
|
+
static useShadowDOM = true;
|
|
4
|
+
static get observedAttributes() {
|
|
5
|
+
return ['variant', 'width', 'height', 'lines', 'animate'];
|
|
6
|
+
}
|
|
7
|
+
template() {
|
|
8
|
+
const variant = this.getAttribute('variant') || 'text';
|
|
9
|
+
const width = this.getAttribute('width') || '100%';
|
|
10
|
+
const height = this.getAttribute('height') || '';
|
|
11
|
+
const lines = Math.max(1, Number(this.getAttribute('lines') || 1));
|
|
12
|
+
const animate = this.getAttribute('animate') ?? 'wave';
|
|
13
|
+
const baseHeight = height || (variant === 'text'
|
|
14
|
+
? '0.875em'
|
|
15
|
+
: variant === 'rect'
|
|
16
|
+
? '120px'
|
|
17
|
+
: variant === 'circle'
|
|
18
|
+
? width
|
|
19
|
+
: '160px');
|
|
20
|
+
let content = '';
|
|
21
|
+
if (variant === 'text') {
|
|
22
|
+
content = Array.from({ length: lines }, (_, index) => {
|
|
23
|
+
const lineWidth = index === lines - 1 && lines > 1 ? '75%' : '100%';
|
|
24
|
+
return `<span class="bone bone--text" style="width:${lineWidth}"></span>`;
|
|
25
|
+
}).join('');
|
|
26
|
+
}
|
|
27
|
+
else if (variant === 'circle') {
|
|
28
|
+
content = `<span class="bone bone--circle" style="width:${width};height:${width}"></span>`;
|
|
29
|
+
}
|
|
30
|
+
else if (variant === 'card') {
|
|
31
|
+
content = `
|
|
32
|
+
<span class="bone bone--rect" style="height:120px;margin-bottom:12px"></span>
|
|
33
|
+
<span class="bone bone--text" style="width:60%;margin-bottom:8px"></span>
|
|
34
|
+
<span class="bone bone--text" style="width:90%;margin-bottom:8px"></span>
|
|
35
|
+
<span class="bone bone--text" style="width:75%"></span>`;
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
content = `<span class="bone bone--rect" style="height:${baseHeight}"></span>`;
|
|
39
|
+
}
|
|
40
|
+
return `
|
|
41
|
+
<style>
|
|
42
|
+
:host { display: block; width: ${variant === 'circle' ? 'auto' : width}; }
|
|
43
|
+
|
|
44
|
+
.skeleton {
|
|
45
|
+
display: flex;
|
|
46
|
+
flex-direction: column;
|
|
47
|
+
gap: 6px;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
.bone {
|
|
51
|
+
display: block;
|
|
52
|
+
background: var(--nc-bg-tertiary, #e5e7eb);
|
|
53
|
+
border-radius: 4px;
|
|
54
|
+
position: relative;
|
|
55
|
+
overflow: hidden;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
.bone--text { height: ${baseHeight}; border-radius: 3px; }
|
|
59
|
+
.bone--circle { border-radius: 50%; flex-shrink: 0; }
|
|
60
|
+
.bone--rect { border-radius: var(--nc-radius-md, 8px); width: 100%; }
|
|
61
|
+
|
|
62
|
+
${animate === 'wave' ? `
|
|
63
|
+
@keyframes nc-skeleton-wave {
|
|
64
|
+
0% { transform: translateX(-100%); }
|
|
65
|
+
100% { transform: translateX(100%); }
|
|
66
|
+
}
|
|
67
|
+
.bone::after {
|
|
68
|
+
content: '';
|
|
69
|
+
position: absolute;
|
|
70
|
+
inset: 0;
|
|
71
|
+
background: linear-gradient(90deg, transparent 0%, rgba(255,255,255,.08) 50%, transparent 100%);
|
|
72
|
+
animation: nc-skeleton-wave 1.6s ease-in-out infinite;
|
|
73
|
+
}` : ''}
|
|
74
|
+
|
|
75
|
+
${animate === 'pulse' ? `
|
|
76
|
+
@keyframes nc-skeleton-pulse {
|
|
77
|
+
0%, 100% { opacity: 1; }
|
|
78
|
+
50% { opacity: 0.4; }
|
|
79
|
+
}
|
|
80
|
+
.bone { animation: nc-skeleton-pulse 1.8s ease-in-out infinite; }` : ''}
|
|
81
|
+
</style>
|
|
82
|
+
<div class="skeleton" aria-hidden="true">${content}</div>
|
|
83
|
+
`;
|
|
84
|
+
}
|
|
85
|
+
attributeChangedCallback(name, oldValue, newValue) {
|
|
86
|
+
if (oldValue !== newValue && this._mounted)
|
|
87
|
+
this.render();
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
defineComponent('nc-skeleton', NcSkeleton);
|