noph-ui 0.16.20 → 0.16.22

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.
@@ -526,6 +526,7 @@
526
526
  }
527
527
 
528
528
  .content select {
529
+ position: absolute;
529
530
  width: 0;
530
531
  height: 0;
531
532
  visibility: hidden;
@@ -0,0 +1,196 @@
1
+ <script lang="ts">
2
+ import Ripple from '../ripple/Ripple.svelte'
3
+ import { getContext } from 'svelte'
4
+ import type { TabProps } from './types.ts'
5
+
6
+ let {
7
+ children,
8
+ inlineIcon = false,
9
+ icon,
10
+ onclick,
11
+ onkeydown,
12
+ value,
13
+ variant = 'primary',
14
+ ...attributes
15
+ }: TabProps = $props()
16
+ let activeTab: { value: string | number; node: HTMLElement } = getContext('activeTab')
17
+ let isActive = $derived(activeTab.value === value)
18
+
19
+ const setTabActive = (el: HTMLDivElement) => {
20
+ const oldTab = activeTab.node as HTMLElement | undefined
21
+ const oldIndicator = oldTab?.querySelector('.np-indicator') as HTMLElement
22
+ const oldIndicatorRect = oldIndicator?.getBoundingClientRect()
23
+ if (oldIndicatorRect) {
24
+ const newIndicator = el.querySelector('.np-indicator') as HTMLElement
25
+ newIndicator.style.setProperty(
26
+ '--np-tab-indicator-start',
27
+ `${oldIndicatorRect.x - newIndicator.getBoundingClientRect().x}px`,
28
+ )
29
+ }
30
+ activeTab.value = value
31
+ activeTab.node = el
32
+ }
33
+ </script>
34
+
35
+ <div
36
+ {@attach (el) => {
37
+ if (isActive) {
38
+ activeTab.node = el
39
+ }
40
+ }}
41
+ {...attributes}
42
+ tabindex={isActive ? 0 : -1}
43
+ role="tab"
44
+ class={[
45
+ 'np-tab',
46
+ variant === 'secondary' && 'np-tab-secondary',
47
+ isActive && 'np-tab-content-active',
48
+ attributes.class,
49
+ ]}
50
+ onclick={(event) => {
51
+ setTabActive(event.currentTarget)
52
+ if (onclick) {
53
+ onclick(event)
54
+ }
55
+ }}
56
+ onkeydown={(event) => {
57
+ if (event.key === 'Enter' || event.key === ' ') {
58
+ setTabActive(event.currentTarget)
59
+ }
60
+ if (onkeydown) {
61
+ onkeydown(event)
62
+ }
63
+ }}
64
+ >
65
+ <div
66
+ class="np-tab-content"
67
+ style={variant === 'secondary' ? '--np-indicator-radius: 0;--_indicator-gap: 0' : ''}
68
+ >
69
+ <div
70
+ class={[
71
+ 'np-tab-label',
72
+ !inlineIcon && variant === 'primary' && children && icon && 'np-tab-no-inline',
73
+ ]}
74
+ >
75
+ {@render icon?.()}
76
+ {@render children?.()}
77
+ {#if variant === 'primary'}
78
+ <div class="np-indicator"></div>
79
+ {/if}
80
+ </div>
81
+ {#if variant === 'secondary'}
82
+ <div class="np-indicator"></div>
83
+ {/if}
84
+ </div>
85
+ <div class="focus-area"></div>
86
+ <Ripple />
87
+ </div>
88
+
89
+ <style>
90
+ .np-tab {
91
+ flex: 1;
92
+ display: flex;
93
+ align-items: center;
94
+ justify-content: center;
95
+ font-family: inherit;
96
+ box-sizing: border-box;
97
+ -webkit-tap-highlight-color: rgba(0, 0, 0, 0);
98
+ background-color: transparent;
99
+ border-width: 0;
100
+ position: relative;
101
+ cursor: pointer;
102
+ padding: 0 1rem;
103
+ color: var(--np-color-on-surface-variant);
104
+ height: 3rem;
105
+ }
106
+ .np-tab-content-active {
107
+ color: var(--np-color-primary);
108
+ --_focus-bottom: 4px;
109
+ }
110
+ .np-tab-content {
111
+ height: 100%;
112
+ }
113
+ .np-tab-label {
114
+ display: flex;
115
+ align-items: center;
116
+ height: 100%;
117
+ width: 100%;
118
+ position: relative;
119
+ font-size: 0.875rem;
120
+ line-height: 1.25rem;
121
+ font-weight: 500;
122
+ gap: 0.5rem;
123
+ text-wrap: nowrap;
124
+ min-width: 1.5rem;
125
+ }
126
+
127
+ .np-tab-no-inline {
128
+ flex-direction: column;
129
+ gap: 2px;
130
+ justify-content: center;
131
+ }
132
+ .np-tab:has(.np-tab-no-inline) {
133
+ height: 4rem;
134
+ }
135
+
136
+ .np-indicator {
137
+ position: absolute;
138
+ bottom: 0;
139
+ left: var(--_indicator-gap, 2px);
140
+ right: var(--_indicator-gap, 2px);
141
+ height: 3px;
142
+ background-color: var(--np-color-primary);
143
+ border-top-left-radius: var(--np-indicator-radius, var(--np-shape-corner-full));
144
+ border-top-right-radius: var(--np-indicator-radius, var(--np-shape-corner-full));
145
+ opacity: 0;
146
+ }
147
+ .np-tab-content-active .np-indicator {
148
+ opacity: 1;
149
+ animation: slide 0.3s ease-in-out;
150
+ }
151
+
152
+ @keyframes slide {
153
+ 0% {
154
+ transform: translateX(var(--np-tab-indicator-start));
155
+ }
156
+ 100% {
157
+ transform: translateX(0);
158
+ }
159
+ }
160
+
161
+ .focus-area {
162
+ position: absolute;
163
+ top: 0;
164
+ left: 0;
165
+ right: 0;
166
+ bottom: var(--_focus-bottom, 0);
167
+ border-radius: var(--np-shape-corner-medium);
168
+ transition: height 0.3s ease;
169
+ pointer-events: none;
170
+ }
171
+ .np-tab:focus-visible {
172
+ outline: none;
173
+ }
174
+
175
+ .np-tab:focus-visible .focus-area {
176
+ outline-style: solid;
177
+ outline-color: var(--np-color-primary);
178
+ outline-width: 3px;
179
+ outline-offset: -3px;
180
+ animation: focusAnimation 0.3s ease forwards;
181
+ }
182
+ @keyframes focusAnimation {
183
+ 0% {
184
+ outline-offset: -3px;
185
+ outline-width: 3px;
186
+ }
187
+ 50% {
188
+ outline-offset: -6px;
189
+ outline-width: 6px;
190
+ }
191
+ 100% {
192
+ outline-offset: -3px;
193
+ outline-width: 3px;
194
+ }
195
+ }
196
+ </style>
@@ -0,0 +1,4 @@
1
+ import type { TabProps } from './types.ts';
2
+ declare const Tab: import("svelte").Component<TabProps, {}, "">;
3
+ type Tab = ReturnType<typeof Tab>;
4
+ export default Tab;
@@ -1,5 +1,62 @@
1
1
  <script lang="ts">
2
- const { children } = $props()
2
+ import Divider from '../divider/Divider.svelte'
3
+ import { setContext } from 'svelte'
4
+ import type { TabsProps } from './types.ts'
5
+
6
+ let {
7
+ children,
8
+ element = $bindable(),
9
+ value = $bindable(),
10
+ onkeydown,
11
+ ...attributes
12
+ }: TabsProps = $props()
13
+ let active = $state({
14
+ value: value,
15
+ node: element?.firstChild as HTMLElement | undefined,
16
+ })
17
+ setContext('activeTab', active)
18
+
19
+ $effect(() => {
20
+ value = active.value
21
+ })
3
22
  </script>
4
23
 
5
- {@render children?.()}
24
+ <div>
25
+ <div
26
+ {...attributes}
27
+ class={['np-tabs', attributes.class]}
28
+ role="tablist"
29
+ tabindex="-1"
30
+ bind:this={element}
31
+ onkeydown={(event) => {
32
+ if (event.key === 'ArrowRight' || event.key === 'ArrowLeft') {
33
+ const tabs: HTMLElement[] = Array.from(event.currentTarget.querySelectorAll('.np-tab'))
34
+ const focusedTab = event.currentTarget.querySelector('.np-tab:focus') as HTMLElement
35
+ const currentIndex = tabs.indexOf(focusedTab)
36
+ const index = currentIndex + (event.key === 'ArrowRight' ? 1 : -1)
37
+ const newTab =
38
+ index < 0 ? tabs[tabs.length - 1] : index >= tabs.length ? tabs[0] : tabs[index]
39
+ newTab.focus()
40
+ }
41
+ if (onkeydown) {
42
+ onkeydown(event)
43
+ }
44
+ }}
45
+ >
46
+ {@render children?.()}
47
+ </div>
48
+ <Divider />
49
+ </div>
50
+
51
+ <style>
52
+ .np-tabs {
53
+ display: flex;
54
+ align-items: end;
55
+ width: 100%;
56
+ height: 100%;
57
+ scrollbar-width: none;
58
+ scroll-behavior: smooth;
59
+ overflow: auto;
60
+ background-color: var(--np-color-surface);
61
+ }
62
+ </style>
@@ -1,5 +1,4 @@
1
- declare const Tabs: import("svelte").Component<{
2
- children: any;
3
- }, {}, "">;
1
+ import type { TabsProps } from './types.ts';
2
+ declare const Tabs: import("svelte").Component<TabsProps, {}, "element" | "value">;
4
3
  type Tabs = ReturnType<typeof Tabs>;
5
4
  export default Tabs;
@@ -1,3 +1,2 @@
1
1
  export { default as Tabs } from './Tabs.svelte';
2
- export { default as PrimaryTab } from './PrimaryTab.svelte';
3
- export { default as SecondaryTab } from './SecondaryTab.svelte';
2
+ export { default as Tab } from './Tab.svelte';
@@ -1,3 +1,2 @@
1
1
  export { default as Tabs } from './Tabs.svelte';
2
- export { default as PrimaryTab } from './PrimaryTab.svelte';
3
- export { default as SecondaryTab } from './SecondaryTab.svelte';
2
+ export { default as Tab } from './Tab.svelte';
@@ -0,0 +1,12 @@
1
+ import type { Snippet } from 'svelte';
2
+ import type { HTMLAttributes } from 'svelte/elements';
3
+ export interface TabProps extends HTMLAttributes<HTMLDivElement> {
4
+ variant?: 'primary' | 'secondary';
5
+ inlineIcon?: boolean;
6
+ value: string | number;
7
+ icon?: Snippet;
8
+ }
9
+ export interface TabsProps extends HTMLAttributes<HTMLDivElement> {
10
+ value: string | number;
11
+ element?: HTMLElement;
12
+ }
@@ -0,0 +1 @@
1
+ export {};
package/dist/types.d.ts CHANGED
@@ -15,3 +15,4 @@ export * from './select/types.js';
15
15
  export * from './snackbar/types.ts';
16
16
  export * from './text-field/types.ts';
17
17
  export * from './tooltip/types.ts';
18
+ export * from './tabs/types.ts';
package/dist/types.js CHANGED
@@ -15,3 +15,4 @@ export * from './select/types.js';
15
15
  export * from './snackbar/types.ts';
16
16
  export * from './text-field/types.ts';
17
17
  export * from './tooltip/types.ts';
18
+ export * from './tabs/types.ts';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "noph-ui",
3
- "version": "0.16.20",
3
+ "version": "0.16.22",
4
4
  "license": "MIT",
5
5
  "homepage": "https://noph.dev",
6
6
  "repository": {
@@ -50,7 +50,7 @@
50
50
  "!dist/**/*.spec.*"
51
51
  ],
52
52
  "peerDependencies": {
53
- "svelte": "^5.20.0"
53
+ "svelte": "^5.32.1"
54
54
  },
55
55
  "devDependencies": {
56
56
  "@eslint/js": "^9.27.0",
@@ -63,12 +63,12 @@
63
63
  "@types/eslint": "^9.6.1",
64
64
  "eslint": "^9.27.0",
65
65
  "eslint-config-prettier": "^10.1.5",
66
- "eslint-plugin-svelte": "^3.8.2",
66
+ "eslint-plugin-svelte": "^3.9.0",
67
67
  "globals": "^16.1.0",
68
68
  "prettier": "^3.5.3",
69
69
  "prettier-plugin-svelte": "^3.4.0",
70
70
  "publint": "^0.3.12",
71
- "svelte": "^5.32.0",
71
+ "svelte": "^5.32.1",
72
72
  "svelte-check": "^4.2.1",
73
73
  "typescript": "^5.8.3",
74
74
  "typescript-eslint": "^8.32.1",
@@ -1,5 +0,0 @@
1
- <script lang="ts">
2
- const { children } = $props()
3
- </script>
4
-
5
- {@render children?.()}
@@ -1,5 +0,0 @@
1
- declare const PrimaryTab: import("svelte").Component<{
2
- children: any;
3
- }, {}, "">;
4
- type PrimaryTab = ReturnType<typeof PrimaryTab>;
5
- export default PrimaryTab;
@@ -1,5 +0,0 @@
1
- <script lang="ts">
2
- const { children } = $props()
3
- </script>
4
-
5
- {@render children?.()}
@@ -1,5 +0,0 @@
1
- declare const SecondaryTab: import("svelte").Component<{
2
- children: any;
3
- }, {}, "">;
4
- type SecondaryTab = ReturnType<typeof SecondaryTab>;
5
- export default SecondaryTab;