@devslab/dds-svelte 0.0.0-stage → 0.29.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/LICENSE +74 -0
- package/README.ko.md +21 -0
- package/README.md +25 -2
- package/dist/ConsoleShell.svelte +57 -0
- package/dist/ConsoleShell.svelte.d.ts +4 -0
- package/dist/Layout.svelte +152 -0
- package/dist/Layout.svelte.d.ts +4 -0
- package/dist/LeftNav.svelte +93 -0
- package/dist/LeftNav.svelte.d.ts +4 -0
- package/dist/TopNav.svelte +75 -0
- package/dist/TopNav.svelte.d.ts +4 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +4 -0
- package/dist/types.d.ts +72 -0
- package/dist/types.js +1 -0
- package/package.json +58 -3
- package/styles.css +2 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
DevsLab Source-Available License 1.0
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 DevsLab. All rights reserved.
|
|
4
|
+
|
|
5
|
+
This license governs the DDS software, design tokens, components, icons,
|
|
6
|
+
documentation, and other materials distributed with this license (the
|
|
7
|
+
"Software").
|
|
8
|
+
|
|
9
|
+
1. Permitted use
|
|
10
|
+
|
|
11
|
+
Subject to this license, DevsLab grants you a limited, worldwide,
|
|
12
|
+
non-exclusive, non-transferable, revocable, royalty-free right to:
|
|
13
|
+
|
|
14
|
+
- download and install unmodified copies of the Software;
|
|
15
|
+
- use and execute the Software for internal or commercial applications;
|
|
16
|
+
- compile, minify, tree-shake, and bundle unmodified Software solely as a
|
|
17
|
+
technical part of building an application; and
|
|
18
|
+
- distribute the resulting object code only as an inseparable part of that
|
|
19
|
+
application and not as a standalone library, design system, component kit,
|
|
20
|
+
template, or asset collection.
|
|
21
|
+
|
|
22
|
+
Normal application configuration through documented public APIs, design-token
|
|
23
|
+
overrides, CSS custom properties, and component properties is permitted and is
|
|
24
|
+
not considered modification of the Software.
|
|
25
|
+
|
|
26
|
+
2. Restrictions
|
|
27
|
+
|
|
28
|
+
Except where applicable law does not allow the restriction, you may not:
|
|
29
|
+
|
|
30
|
+
- modify, adapt, translate, reverse engineer, or create derivative works of the
|
|
31
|
+
Software itself;
|
|
32
|
+
- publish, republish, sublicense, sell, rent, host, or redistribute the
|
|
33
|
+
Software or any modified version as a standalone package or downloadable
|
|
34
|
+
collection;
|
|
35
|
+
- use the Software or a substantial portion of it to create or operate a
|
|
36
|
+
competing general-purpose design system, UI kit, component library, icon
|
|
37
|
+
library, token library, template marketplace, or hosted equivalent;
|
|
38
|
+
- remove or alter copyright, license, attribution, or provenance notices; or
|
|
39
|
+
- use DevsLab names, logos, product marks, or trade dress except to make a
|
|
40
|
+
truthful statement that an application uses the Software.
|
|
41
|
+
|
|
42
|
+
No patent, trademark, design, or branding rights are granted except the
|
|
43
|
+
copyright permissions stated in Section 1.
|
|
44
|
+
|
|
45
|
+
3. Ownership and feedback
|
|
46
|
+
|
|
47
|
+
The Software is licensed, not sold. DevsLab and its licensors retain all
|
|
48
|
+
right, title, and interest in the Software. You grant DevsLab a perpetual,
|
|
49
|
+
worldwide, irrevocable, royalty-free right to use feedback you voluntarily
|
|
50
|
+
provide about the Software.
|
|
51
|
+
|
|
52
|
+
4. Termination
|
|
53
|
+
|
|
54
|
+
This license terminates automatically if you breach it. Upon termination, you
|
|
55
|
+
must stop using and distributing the Software and delete all copies under your
|
|
56
|
+
control. Sections 2, 3, 5, and 6 survive termination.
|
|
57
|
+
|
|
58
|
+
5. Disclaimer and limitation of liability
|
|
59
|
+
|
|
60
|
+
THE SOFTWARE IS PROVIDED "AS IS" WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
|
|
61
|
+
EXPRESS OR IMPLIED, INCLUDING MERCHANTABILITY, FITNESS FOR A PARTICULAR
|
|
62
|
+
PURPOSE, TITLE, AND NON-INFRINGEMENT. TO THE MAXIMUM EXTENT PERMITTED BY LAW,
|
|
63
|
+
DEVSLAB WILL NOT BE LIABLE FOR ANY INDIRECT, INCIDENTAL, SPECIAL,
|
|
64
|
+
CONSEQUENTIAL, EXEMPLARY, OR PUNITIVE DAMAGES, OR FOR LOSS OF DATA, PROFITS,
|
|
65
|
+
REVENUE, OR BUSINESS, ARISING FROM OR RELATED TO THE SOFTWARE.
|
|
66
|
+
|
|
67
|
+
6. Governing law
|
|
68
|
+
|
|
69
|
+
This license is governed by the laws of the Republic of Korea, excluding
|
|
70
|
+
conflict-of-law rules. The Seoul Central District Court has exclusive
|
|
71
|
+
jurisdiction over disputes arising from this license.
|
|
72
|
+
|
|
73
|
+
This is a source-available license, not an open-source license approved by the
|
|
74
|
+
Open Source Initiative.
|
package/README.ko.md
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# @devslab/dds-svelte
|
|
2
|
+
|
|
3
|
+
공통 DDS 간격·타이포·CSS를 사용하는 Svelte 5 Layout·LeftNav·TopNav·ConsoleShell입니다.
|
|
4
|
+
현재 이 화면 구성 요소만 제공합니다. 다른 프레임워크의 전체 컨트롤·그리드가 포함되었다는 의미가 아닙니다.
|
|
5
|
+
|
|
6
|
+
`@devslab/dds-svelte/styles.css`를 한 번 import하세요. Layout의 navigationPosition은
|
|
7
|
+
left/top, contentWidth는 workspace/form/settings/full입니다. header/navigation/railStart/
|
|
8
|
+
railEnd/footer와 본문은 Svelte snippet으로 조립합니다. 메뉴는 레이아웃 없이도 사용합니다.
|
|
9
|
+
아이콘은 장식용 snippet이고 라벨이 접근 가능한 이름을 제공합니다.
|
|
10
|
+
|
|
11
|
+
LeftNav의 expandedGroups/onExpandedGroupsChange는 외부 제어, defaultExpandedGroups는
|
|
12
|
+
초기값입니다. 빈 배열은 모두 접고 생략하면 활성 그룹만 엽니다. 앱은 권한·라우팅·이동 시
|
|
13
|
+
그룹 열기·세션 보관을 담당합니다. 브라우저 저장소는 hydration 이후 읽으세요.
|
|
14
|
+
Layout은 모바일 탐색과 초점 복귀를 담당하고 TopNav는 네이티브 disclosure를 사용합니다.
|
|
15
|
+
|
|
16
|
+
[공통 화면 규격](../../docs/layouts.ko.md)을 참고하세요. 배포 전 test/check/build로 검증합니다.
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
## 공통 업무 화면
|
|
20
|
+
|
|
21
|
+
Layout·LeftNav·TopNav는 공통 간격과 타이포 규격을 사용합니다. [조립 방법과 수치](../../docs/layouts.ko.md)를 참고하세요.
|
package/README.md
CHANGED
|
@@ -1,3 +1,26 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @devslab/dds-svelte
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Svelte 5 Layout, LeftNav, TopNav and ConsoleShell using the shared DDS geometry,
|
|
4
|
+
typography and CSS. This package currently provides these application shell
|
|
5
|
+
components; it does not claim the React/Solid control or grid inventory.
|
|
6
|
+
|
|
7
|
+
Import `@devslab/dds-svelte/styles.css` once. Use `Layout` with
|
|
8
|
+
`navigationPosition="left"` or `"top"`, `contentWidth="workspace"|"form"|"settings"|"full"`,
|
|
9
|
+
and header/navigation/railStart/railEnd/footer snippets plus children.
|
|
10
|
+
`LeftNav` and `TopNav` work independently of Layout. Icons are decorative snippets.
|
|
11
|
+
|
|
12
|
+
LeftNav supports nav groups with stable IDs and optional collapsible disclosure.
|
|
13
|
+
Use expandedGroups/onExpandedGroupsChange for parent control, or
|
|
14
|
+
defaultExpandedGroups for initial state. Empty defaults close all groups;
|
|
15
|
+
otherwise active groups initially open. The app owns routing, permissions,
|
|
16
|
+
route-change expansion and storage; read browser storage after hydration.
|
|
17
|
+
Layout owns the shared mobile navigation and focus restoration. TopNav uses
|
|
18
|
+
native disclosure navigation, not a partial application menubar.
|
|
19
|
+
|
|
20
|
+
See [shared layout guidance](../../docs/layouts.md) for exact dimensions and
|
|
21
|
+
composition examples. Run package test/check/build before publication.
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
## Shared application layouts
|
|
25
|
+
|
|
26
|
+
Layout, LeftNav and TopNav use the common geometry and typography rules. See [composition and exact dimensions](../../docs/layouts.md).
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import Layout from "./Layout.svelte";
|
|
3
|
+
import LeftNav from "./LeftNav.svelte";
|
|
4
|
+
import type { ConsoleShellProps } from "./types";
|
|
5
|
+
let {
|
|
6
|
+
surface,
|
|
7
|
+
activePath,
|
|
8
|
+
brand,
|
|
9
|
+
context,
|
|
10
|
+
nav = [],
|
|
11
|
+
navigation: customNavigation,
|
|
12
|
+
labels,
|
|
13
|
+
header: pageHeader,
|
|
14
|
+
foot,
|
|
15
|
+
children,
|
|
16
|
+
expandedGroups,
|
|
17
|
+
defaultExpandedGroups,
|
|
18
|
+
onExpandedGroupsChange,
|
|
19
|
+
}: ConsoleShellProps = $props();
|
|
20
|
+
</script>
|
|
21
|
+
|
|
22
|
+
<Layout {surface} {labels}>
|
|
23
|
+
{#snippet railStart()}<a class="dds-console-rail__brand" href={brand.href}
|
|
24
|
+
><img src={brand.mark} alt="" width="20" height="20" /><strong
|
|
25
|
+
>{brand.name}</strong
|
|
26
|
+
></a
|
|
27
|
+
>{#if context?.length}<dl class="dds-console-rail__context">
|
|
28
|
+
{#each context as row, index (index)}<div>
|
|
29
|
+
<dt>{row.label}</dt>
|
|
30
|
+
<dd>{row.value}</dd>
|
|
31
|
+
</div>{/each}
|
|
32
|
+
</dl>{/if}{/snippet}
|
|
33
|
+
{#snippet navigation()}{#if customNavigation}{@render customNavigation()}{:else}<LeftNav
|
|
34
|
+
{surface}
|
|
35
|
+
{activePath}
|
|
36
|
+
{nav}
|
|
37
|
+
{labels}
|
|
38
|
+
{expandedGroups}
|
|
39
|
+
{defaultExpandedGroups}
|
|
40
|
+
{onExpandedGroupsChange}
|
|
41
|
+
/>{/if}{/snippet}
|
|
42
|
+
{#snippet railEnd()}{#if foot}<div class="dds-console-rail__foot">
|
|
43
|
+
{@render foot()}
|
|
44
|
+
</div>{/if}{/snippet}
|
|
45
|
+
{#snippet header()}<div class="dds-console-page-header">
|
|
46
|
+
<div class="dds-console-page-header__text">
|
|
47
|
+
{#if pageHeader.eyebrow}<p class="dds-console-page-eyebrow">
|
|
48
|
+
{pageHeader.eyebrow}
|
|
49
|
+
</p>{/if}
|
|
50
|
+
<h1>{pageHeader.title}</h1>
|
|
51
|
+
</div>
|
|
52
|
+
{#if pageHeader.actions}<div class="dds-console-page-header__actions">
|
|
53
|
+
{@render pageHeader.actions()}
|
|
54
|
+
</div>{/if}
|
|
55
|
+
</div>{/snippet}
|
|
56
|
+
{@render children?.()}
|
|
57
|
+
</Layout>
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { onMount, tick } from "svelte";
|
|
3
|
+
import type { LayoutProps } from "./types";
|
|
4
|
+
let {
|
|
5
|
+
navigationPosition = "left",
|
|
6
|
+
contentWidth = "workspace",
|
|
7
|
+
labels,
|
|
8
|
+
surface,
|
|
9
|
+
header,
|
|
10
|
+
navigation,
|
|
11
|
+
railStart,
|
|
12
|
+
railEnd,
|
|
13
|
+
footer,
|
|
14
|
+
children,
|
|
15
|
+
}: LayoutProps = $props();
|
|
16
|
+
const id = $props.id(),
|
|
17
|
+
railId = `dds-console-rail-${id}`,
|
|
18
|
+
mainId = `dds-console-main-${id}`;
|
|
19
|
+
let open = $state(false),
|
|
20
|
+
hydrated = $state(false),
|
|
21
|
+
opener: HTMLButtonElement,
|
|
22
|
+
rail: HTMLElement;
|
|
23
|
+
function close() {
|
|
24
|
+
open = false;
|
|
25
|
+
opener?.focus();
|
|
26
|
+
}
|
|
27
|
+
function focusables() {
|
|
28
|
+
return [
|
|
29
|
+
opener,
|
|
30
|
+
...Array.from(
|
|
31
|
+
rail?.querySelectorAll<HTMLElement>(
|
|
32
|
+
'a[href],button,summary,input,select,textarea,[tabindex="0"]',
|
|
33
|
+
) ?? [],
|
|
34
|
+
),
|
|
35
|
+
].filter(
|
|
36
|
+
(el) =>
|
|
37
|
+
el &&
|
|
38
|
+
!el.closest("[hidden]") &&
|
|
39
|
+
el.getClientRects().length > 0 &&
|
|
40
|
+
getComputedStyle(el).visibility !== "hidden" &&
|
|
41
|
+
!Array.from(rail?.querySelectorAll("details:not([open])") ?? []).some(
|
|
42
|
+
(details) =>
|
|
43
|
+
details.contains(el) &&
|
|
44
|
+
!details.querySelector("summary")?.contains(el),
|
|
45
|
+
) &&
|
|
46
|
+
!el.hasAttribute("disabled"),
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
async function toggle() {
|
|
50
|
+
if (open) close();
|
|
51
|
+
else {
|
|
52
|
+
open = true;
|
|
53
|
+
await tick();
|
|
54
|
+
focusables()[1]?.focus();
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
function key(event: KeyboardEvent) {
|
|
58
|
+
if (!open) return;
|
|
59
|
+
if (event.key === "Escape") {
|
|
60
|
+
event.preventDefault();
|
|
61
|
+
close();
|
|
62
|
+
}
|
|
63
|
+
if (event.key === "Tab") {
|
|
64
|
+
const items = focusables(),
|
|
65
|
+
first = items[0],
|
|
66
|
+
last = items.at(-1);
|
|
67
|
+
if (event.shiftKey && document.activeElement === first) {
|
|
68
|
+
event.preventDefault();
|
|
69
|
+
last?.focus();
|
|
70
|
+
} else if (!event.shiftKey && document.activeElement === last) {
|
|
71
|
+
event.preventDefault();
|
|
72
|
+
first?.focus();
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
function navigate(event: MouseEvent) {
|
|
77
|
+
if (
|
|
78
|
+
open &&
|
|
79
|
+
event.target instanceof Element &&
|
|
80
|
+
event.target.closest("a[href]")
|
|
81
|
+
)
|
|
82
|
+
close();
|
|
83
|
+
}
|
|
84
|
+
onMount(() => {
|
|
85
|
+
hydrated = true;
|
|
86
|
+
const media =
|
|
87
|
+
typeof window.matchMedia === "function"
|
|
88
|
+
? window.matchMedia("(max-width: 56.25rem)")
|
|
89
|
+
: undefined;
|
|
90
|
+
const resize = () => {
|
|
91
|
+
if (!media?.matches) open = false;
|
|
92
|
+
};
|
|
93
|
+
media?.addEventListener("change", resize);
|
|
94
|
+
document.addEventListener("keydown", key);
|
|
95
|
+
return () => {
|
|
96
|
+
document.removeEventListener("keydown", key);
|
|
97
|
+
media?.removeEventListener("change", resize);
|
|
98
|
+
};
|
|
99
|
+
});
|
|
100
|
+
</script>
|
|
101
|
+
|
|
102
|
+
<div
|
|
103
|
+
class="dds-console-shell dds-layout"
|
|
104
|
+
data-navigation-position={navigationPosition}
|
|
105
|
+
data-surface={surface}
|
|
106
|
+
data-hydrated={hydrated ? "true" : undefined}
|
|
107
|
+
>
|
|
108
|
+
<a class="dds-console-skip dds-visually-hidden" href={`#${mainId}`}
|
|
109
|
+
>{labels.skip}</a
|
|
110
|
+
>
|
|
111
|
+
<button
|
|
112
|
+
bind:this={opener}
|
|
113
|
+
class="dds-console-rail__toggle"
|
|
114
|
+
type="button"
|
|
115
|
+
data-action="toggle-rail"
|
|
116
|
+
aria-controls={railId}
|
|
117
|
+
aria-expanded={open}
|
|
118
|
+
onclick={toggle}
|
|
119
|
+
><span aria-hidden="true"></span><span class="dds-visually-hidden"
|
|
120
|
+
>{open ? labels.menuClose : labels.menuOpen}</span
|
|
121
|
+
></button
|
|
122
|
+
>
|
|
123
|
+
<button
|
|
124
|
+
type="button"
|
|
125
|
+
tabindex="-1"
|
|
126
|
+
aria-label={labels.menuClose}
|
|
127
|
+
class="dds-console-rail__scrim"
|
|
128
|
+
data-open={String(open)}
|
|
129
|
+
onclick={close}
|
|
130
|
+
></button>
|
|
131
|
+
<!-- svelte-ignore a11y_no_noninteractive_element_interactions, a11y_click_events_have_key_events -->
|
|
132
|
+
<aside
|
|
133
|
+
bind:this={rail}
|
|
134
|
+
id={railId}
|
|
135
|
+
class="dds-console-rail dds-layout__navigation"
|
|
136
|
+
data-open={String(open)}
|
|
137
|
+
onclick={navigate}
|
|
138
|
+
>
|
|
139
|
+
{@render railStart?.()}{@render navigation?.()}{@render railEnd?.()}
|
|
140
|
+
</aside>
|
|
141
|
+
<main id={mainId} class="dds-console-main dds-layout__main" tabindex="-1">
|
|
142
|
+
<div class="dds-layout__content" data-width={contentWidth}>
|
|
143
|
+
{#if header}<header class="dds-layout__header">
|
|
144
|
+
{@render header()}
|
|
145
|
+
</header>{/if}{@render children?.()}{#if footer}<footer
|
|
146
|
+
class="dds-layout__footer"
|
|
147
|
+
>
|
|
148
|
+
{@render footer()}
|
|
149
|
+
</footer>{/if}
|
|
150
|
+
</div>
|
|
151
|
+
</main>
|
|
152
|
+
</div>
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { untrack } from "svelte";
|
|
3
|
+
import type { LeftNavProps, ConsoleNavItem } from "./types";
|
|
4
|
+
let {
|
|
5
|
+
surface,
|
|
6
|
+
activePath,
|
|
7
|
+
nav,
|
|
8
|
+
labels,
|
|
9
|
+
expandedGroups,
|
|
10
|
+
defaultExpandedGroups,
|
|
11
|
+
onExpandedGroupsChange,
|
|
12
|
+
onNavigate,
|
|
13
|
+
}: LeftNavProps = $props();
|
|
14
|
+
const id = $props.id();
|
|
15
|
+
function active(item: ConsoleNavItem) {
|
|
16
|
+
return (
|
|
17
|
+
item.active ??
|
|
18
|
+
(activePath === item.href ||
|
|
19
|
+
(item.href.split("/").filter(Boolean).length > 1 &&
|
|
20
|
+
activePath.startsWith(`${item.href}/`)))
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
let internal = $state<readonly string[]>(
|
|
24
|
+
untrack(
|
|
25
|
+
() =>
|
|
26
|
+
defaultExpandedGroups ??
|
|
27
|
+
nav.flatMap((group, index) =>
|
|
28
|
+
group.collapsible && group.items.some(active)
|
|
29
|
+
? [group.id ?? `group-${index}`]
|
|
30
|
+
: [],
|
|
31
|
+
),
|
|
32
|
+
),
|
|
33
|
+
);
|
|
34
|
+
let expanded = $derived(expandedGroups ?? internal);
|
|
35
|
+
function toggle(groupId: string) {
|
|
36
|
+
const next = expanded.includes(groupId)
|
|
37
|
+
? expanded.filter((value) => value !== groupId)
|
|
38
|
+
: [...expanded, groupId];
|
|
39
|
+
if (expandedGroups === undefined) internal = next;
|
|
40
|
+
onExpandedGroupsChange?.(next);
|
|
41
|
+
}
|
|
42
|
+
</script>
|
|
43
|
+
|
|
44
|
+
<nav class="dds-console-rail__nav dds-left-nav" aria-label={labels.nav}>
|
|
45
|
+
{#each nav as group, index (group.id ?? index)}
|
|
46
|
+
<section
|
|
47
|
+
class="dds-console-rail__group"
|
|
48
|
+
aria-labelledby={`${id}-group-${index}`}
|
|
49
|
+
data-collapsible={group.collapsible ? "true" : undefined}
|
|
50
|
+
>
|
|
51
|
+
<h2 id={`${id}-group-${index}`} class="dds-console-rail__group-label">
|
|
52
|
+
{#if group.collapsible}<button
|
|
53
|
+
type="button"
|
|
54
|
+
class="dds-console-rail__group-toggle"
|
|
55
|
+
aria-expanded={expanded.includes(group.id ?? `group-${index}`)}
|
|
56
|
+
aria-controls={`${id}-items-${index}`}
|
|
57
|
+
onclick={() => toggle(group.id ?? `group-${index}`)}
|
|
58
|
+
><span>{group.label}</span><span
|
|
59
|
+
class="dds-console-rail__chevron"
|
|
60
|
+
aria-hidden="true"
|
|
61
|
+
></span></button
|
|
62
|
+
>{:else}{group.label}{/if}
|
|
63
|
+
</h2>
|
|
64
|
+
<ul
|
|
65
|
+
id={`${id}-items-${index}`}
|
|
66
|
+
hidden={group.collapsible &&
|
|
67
|
+
!expanded.includes(group.id ?? `group-${index}`)}
|
|
68
|
+
>
|
|
69
|
+
{#each group.items as item (item.id)}<li>
|
|
70
|
+
<a
|
|
71
|
+
class="dds-console-rail__item"
|
|
72
|
+
data-nav={`${surface}.${item.id}`}
|
|
73
|
+
href={item.href}
|
|
74
|
+
aria-current={active(item) ? "page" : undefined}
|
|
75
|
+
onclick={() => onNavigate?.()}
|
|
76
|
+
>
|
|
77
|
+
{#if item.icon}<span class="dds-console-rail__item-content"
|
|
78
|
+
><span class="dds-console-rail__item-icon" aria-hidden="true"
|
|
79
|
+
>{@render item.icon()}</span
|
|
80
|
+
><span>{item.label}</span></span
|
|
81
|
+
>{:else}<span>{item.label}</span>{/if}
|
|
82
|
+
{#if item.badge != null && item.badge > 0}<span
|
|
83
|
+
class="dds-console-rail__badge"
|
|
84
|
+
aria-label={labels.badge.replace("{count}", () =>
|
|
85
|
+
String(item.badge),
|
|
86
|
+
)}>{item.badge}</span
|
|
87
|
+
>{/if}
|
|
88
|
+
</a>
|
|
89
|
+
</li>{/each}
|
|
90
|
+
</ul>
|
|
91
|
+
</section>
|
|
92
|
+
{/each}
|
|
93
|
+
</nav>
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { onMount } from "svelte";
|
|
3
|
+
import type { TopNavProps, ConsoleNavItem } from "./types";
|
|
4
|
+
let { surface, activePath, nav, labels, onNavigate }: TopNavProps = $props();
|
|
5
|
+
let root: HTMLElement;
|
|
6
|
+
function active(item: ConsoleNavItem) {
|
|
7
|
+
return (
|
|
8
|
+
item.active ??
|
|
9
|
+
(activePath === item.href ||
|
|
10
|
+
(item.href.split("/").filter(Boolean).length > 1 &&
|
|
11
|
+
activePath.startsWith(`${item.href}/`)))
|
|
12
|
+
);
|
|
13
|
+
}
|
|
14
|
+
function outside(event: PointerEvent) {
|
|
15
|
+
if (!root?.contains(event.target as Node))
|
|
16
|
+
root
|
|
17
|
+
?.querySelectorAll("details[open]")
|
|
18
|
+
.forEach((el) => el.removeAttribute("open"));
|
|
19
|
+
}
|
|
20
|
+
function key(event: KeyboardEvent) {
|
|
21
|
+
if (event.key !== "Escape") return;
|
|
22
|
+
const details = (event.target as Element).closest("details");
|
|
23
|
+
if (details?.open) {
|
|
24
|
+
event.preventDefault();
|
|
25
|
+
event.stopPropagation();
|
|
26
|
+
details.open = false;
|
|
27
|
+
details.querySelector("summary")?.focus();
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
function navigate(event: MouseEvent) {
|
|
31
|
+
(event.currentTarget as Element)
|
|
32
|
+
.closest("details")
|
|
33
|
+
?.removeAttribute("open");
|
|
34
|
+
onNavigate?.();
|
|
35
|
+
}
|
|
36
|
+
onMount(() => {
|
|
37
|
+
document.addEventListener("pointerdown", outside);
|
|
38
|
+
return () => document.removeEventListener("pointerdown", outside);
|
|
39
|
+
});
|
|
40
|
+
</script>
|
|
41
|
+
|
|
42
|
+
<!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
|
|
43
|
+
<nav
|
|
44
|
+
bind:this={root}
|
|
45
|
+
class="dds-top-nav"
|
|
46
|
+
aria-label={labels.nav}
|
|
47
|
+
onkeydown={key}
|
|
48
|
+
>
|
|
49
|
+
{#each nav as group, index (group.id ?? index)}<details
|
|
50
|
+
class="dds-top-nav__group"
|
|
51
|
+
>
|
|
52
|
+
<summary class="dds-top-nav__trigger">{group.label}</summary>
|
|
53
|
+
<ul class="dds-top-nav__items">
|
|
54
|
+
{#each group.items as item (item.id)}<li>
|
|
55
|
+
<a
|
|
56
|
+
class="dds-top-nav__item"
|
|
57
|
+
data-nav={`${surface}.${item.id}`}
|
|
58
|
+
href={item.href}
|
|
59
|
+
aria-current={active(item) ? "page" : undefined}
|
|
60
|
+
onclick={navigate}
|
|
61
|
+
>{#if item.icon}<span
|
|
62
|
+
class="dds-top-nav__item-icon"
|
|
63
|
+
aria-hidden="true">{@render item.icon()}</span
|
|
64
|
+
>{/if}<span>{item.label}</span
|
|
65
|
+
>{#if item.badge != null && item.badge > 0}<span
|
|
66
|
+
class="dds-console-rail__badge"
|
|
67
|
+
aria-label={labels.badge.replace("{count}", () =>
|
|
68
|
+
String(item.badge),
|
|
69
|
+
)}>{item.badge}</span
|
|
70
|
+
>{/if}</a
|
|
71
|
+
>
|
|
72
|
+
</li>{/each}
|
|
73
|
+
</ul>
|
|
74
|
+
</details>{/each}
|
|
75
|
+
</nav>
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import type { Snippet } from "svelte";
|
|
2
|
+
export interface ConsoleNavItem {
|
|
3
|
+
id: string;
|
|
4
|
+
href: string;
|
|
5
|
+
label: string;
|
|
6
|
+
badge?: number;
|
|
7
|
+
active?: boolean;
|
|
8
|
+
icon?: Snippet;
|
|
9
|
+
}
|
|
10
|
+
export interface ConsoleNavGroup {
|
|
11
|
+
id?: string;
|
|
12
|
+
label: string;
|
|
13
|
+
items: readonly ConsoleNavItem[];
|
|
14
|
+
collapsible?: boolean;
|
|
15
|
+
}
|
|
16
|
+
export interface ConsoleShellLabels {
|
|
17
|
+
skip: string;
|
|
18
|
+
menuOpen: string;
|
|
19
|
+
menuClose: string;
|
|
20
|
+
nav: string;
|
|
21
|
+
badge: string;
|
|
22
|
+
}
|
|
23
|
+
export interface ConsoleHeader {
|
|
24
|
+
eyebrow?: string;
|
|
25
|
+
title: string;
|
|
26
|
+
actions?: Snippet;
|
|
27
|
+
}
|
|
28
|
+
export interface ConsoleShellProps {
|
|
29
|
+
surface: string;
|
|
30
|
+
activePath: string;
|
|
31
|
+
brand: {
|
|
32
|
+
href: string;
|
|
33
|
+
name: string;
|
|
34
|
+
mark: string;
|
|
35
|
+
};
|
|
36
|
+
context?: readonly {
|
|
37
|
+
label: string;
|
|
38
|
+
value: string;
|
|
39
|
+
}[];
|
|
40
|
+
nav?: readonly ConsoleNavGroup[];
|
|
41
|
+
navigation?: Snippet;
|
|
42
|
+
labels: ConsoleShellLabels;
|
|
43
|
+
header: ConsoleHeader;
|
|
44
|
+
foot?: Snippet;
|
|
45
|
+
children?: Snippet;
|
|
46
|
+
expandedGroups?: readonly string[];
|
|
47
|
+
defaultExpandedGroups?: readonly string[];
|
|
48
|
+
onExpandedGroupsChange?: (groupIds: string[]) => void;
|
|
49
|
+
}
|
|
50
|
+
export interface LeftNavProps {
|
|
51
|
+
surface: string;
|
|
52
|
+
activePath: string;
|
|
53
|
+
nav: readonly ConsoleNavGroup[];
|
|
54
|
+
labels: Pick<ConsoleShellLabels, "nav" | "badge">;
|
|
55
|
+
expandedGroups?: readonly string[];
|
|
56
|
+
defaultExpandedGroups?: readonly string[];
|
|
57
|
+
onExpandedGroupsChange?: (ids: string[]) => void;
|
|
58
|
+
onNavigate?: () => void;
|
|
59
|
+
}
|
|
60
|
+
export type TopNavProps = Pick<LeftNavProps, "surface" | "activePath" | "nav" | "labels" | "onNavigate">;
|
|
61
|
+
export interface LayoutProps {
|
|
62
|
+
navigationPosition?: "left" | "top";
|
|
63
|
+
contentWidth?: "workspace" | "form" | "settings" | "full";
|
|
64
|
+
labels: Pick<ConsoleShellLabels, "skip" | "menuOpen" | "menuClose">;
|
|
65
|
+
surface?: string;
|
|
66
|
+
header?: Snippet;
|
|
67
|
+
navigation?: Snippet;
|
|
68
|
+
railStart?: Snippet;
|
|
69
|
+
railEnd?: Snippet;
|
|
70
|
+
footer?: Snippet;
|
|
71
|
+
children?: Snippet;
|
|
72
|
+
}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
CHANGED
|
@@ -1,6 +1,61 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@devslab/dds-svelte",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.29.0",
|
|
4
|
+
"description": "Svelte 5 ConsoleShell for the DevsLab Design System",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "SEE LICENSE IN LICENSE",
|
|
7
|
+
"sideEffects": [
|
|
8
|
+
"./styles.css"
|
|
9
|
+
],
|
|
10
|
+
"repository": {
|
|
11
|
+
"type": "git",
|
|
12
|
+
"url": "https://github.com/devslab-kr/dds.git",
|
|
13
|
+
"directory": "packages/dds-svelte"
|
|
14
|
+
},
|
|
15
|
+
"publishConfig": {
|
|
16
|
+
"registry": "https://registry.npmjs.org",
|
|
17
|
+
"access": "public",
|
|
18
|
+
"provenance": true
|
|
19
|
+
},
|
|
20
|
+
"files": [
|
|
21
|
+
"dist",
|
|
22
|
+
"styles.css",
|
|
23
|
+
"README.md",
|
|
24
|
+
"README.ko.md"
|
|
25
|
+
],
|
|
26
|
+
"svelte": "./dist/index.js",
|
|
27
|
+
"types": "./dist/index.d.ts",
|
|
28
|
+
"exports": {
|
|
29
|
+
".": {
|
|
30
|
+
"types": "./dist/index.d.ts",
|
|
31
|
+
"svelte": "./dist/index.js",
|
|
32
|
+
"default": "./dist/index.js"
|
|
33
|
+
},
|
|
34
|
+
"./styles.css": "./styles.css"
|
|
35
|
+
},
|
|
36
|
+
"dependencies": {
|
|
37
|
+
"@devslab/dds-css": "0.29.0",
|
|
38
|
+
"@devslab/dds-tokens": "0.29.0"
|
|
39
|
+
},
|
|
40
|
+
"peerDependencies": {
|
|
41
|
+
"svelte": "^5.46.4"
|
|
42
|
+
},
|
|
43
|
+
"devDependencies": {
|
|
44
|
+
"svelte": "5.57.2",
|
|
45
|
+
"@sveltejs/package": "3.0.1",
|
|
46
|
+
"@sveltejs/vite-plugin-svelte": "7.3.1",
|
|
47
|
+
"svelte-check": "4.7.7",
|
|
48
|
+
"typescript": "6.0.3",
|
|
49
|
+
"vite": "8.2.1",
|
|
50
|
+
"vitest": "4.1.1",
|
|
51
|
+
"jsdom": "30.0.1",
|
|
52
|
+
"@testing-library/dom": "10.4.1",
|
|
53
|
+
"@testing-library/user-event": "14.6.1",
|
|
54
|
+
"axe-core": "4.13.0"
|
|
55
|
+
},
|
|
56
|
+
"scripts": {
|
|
57
|
+
"build": "svelte-package",
|
|
58
|
+
"check": "svelte-check --tsconfig ./tsconfig.json",
|
|
59
|
+
"test": "vitest run && vitest run --config vitest.ssr.config.ts"
|
|
60
|
+
}
|
|
6
61
|
}
|
package/styles.css
ADDED