@fivem-nui/svelte-components 0.0.0-stage → 1.0.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 +101 -2
- package/dist/components/actions/NuiButton.svelte +292 -0
- package/dist/components/actions/NuiButton.svelte.d.ts +60 -0
- package/dist/components/data/NuiBadge.svelte +277 -0
- package/dist/components/data/NuiBadge.svelte.d.ts +74 -0
- package/dist/components/data/NuiCodeBlock.svelte +256 -0
- package/dist/components/data/NuiCodeBlock.svelte.d.ts +49 -0
- package/dist/components/data/NuiList.svelte +341 -0
- package/dist/components/data/NuiList.svelte.d.ts +52 -0
- package/dist/components/data/NuiProgressBar.svelte +264 -0
- package/dist/components/data/NuiProgressBar.svelte.d.ts +51 -0
- package/dist/components/data/NuiRichText.svelte +260 -0
- package/dist/components/data/NuiRichText.svelte.d.ts +33 -0
- package/dist/components/data/NuiTable.svelte +470 -0
- package/dist/components/data/NuiTable.svelte.d.ts +53 -0
- package/dist/components/data/chart/NuiChart.svelte +384 -0
- package/dist/components/data/chart/NuiChart.svelte.d.ts +49 -0
- package/dist/components/data/chart/color.d.ts +9 -0
- package/dist/components/data/chart/color.js +78 -0
- package/dist/components/data/chart/index.d.ts +10 -0
- package/dist/components/data/chart/index.js +10 -0
- package/dist/components/data/chart/registry.d.ts +14 -0
- package/dist/components/data/chart/registry.js +53 -0
- package/dist/components/data/chart/renderers/bar.d.ts +8 -0
- package/dist/components/data/chart/renderers/bar.js +79 -0
- package/dist/components/data/chart/renderers/base.d.ts +18 -0
- package/dist/components/data/chart/renderers/base.js +10 -0
- package/dist/components/data/chart/renderers/line.d.ts +8 -0
- package/dist/components/data/chart/renderers/line.js +89 -0
- package/dist/components/data/chart/renderers/pie.d.ts +10 -0
- package/dist/components/data/chart/renderers/pie.js +55 -0
- package/dist/components/data/chart/scale.d.ts +22 -0
- package/dist/components/data/chart/scale.js +101 -0
- package/dist/components/data/chart/types.d.ts +84 -0
- package/dist/components/data/chart/types.js +1 -0
- package/dist/components/data/chart/validator.d.ts +10 -0
- package/dist/components/data/chart/validator.js +82 -0
- package/dist/components/forms/NuiCheckbox.svelte +218 -0
- package/dist/components/forms/NuiCheckbox.svelte.d.ts +37 -0
- package/dist/components/forms/NuiDatepicker.svelte +755 -0
- package/dist/components/forms/NuiDatepicker.svelte.d.ts +34 -0
- package/dist/components/forms/NuiFileInput.svelte +1047 -0
- package/dist/components/forms/NuiFileInput.svelte.d.ts +93 -0
- package/dist/components/forms/NuiFileInput.types.d.ts +28 -0
- package/dist/components/forms/NuiFileInput.types.js +1 -0
- package/dist/components/forms/NuiForm.svelte +391 -0
- package/dist/components/forms/NuiForm.svelte.d.ts +69 -0
- package/dist/components/forms/NuiInput.svelte +296 -0
- package/dist/components/forms/NuiInput.svelte.d.ts +62 -0
- package/dist/components/forms/NuiRadioGroup.svelte +238 -0
- package/dist/components/forms/NuiRadioGroup.svelte.d.ts +47 -0
- package/dist/components/forms/NuiSelect.svelte +819 -0
- package/dist/components/forms/NuiSelect.svelte.d.ts +49 -0
- package/dist/components/layout/NuiCard.svelte +314 -0
- package/dist/components/layout/NuiCard.svelte.d.ts +48 -0
- package/dist/components/layout/NuiCardGrid.svelte +98 -0
- package/dist/components/layout/NuiCardGrid.svelte.d.ts +22 -0
- package/dist/components/layout/NuiFlex.svelte +121 -0
- package/dist/components/layout/NuiFlex.svelte.d.ts +23 -0
- package/dist/components/layout/NuiGrid.svelte +66 -0
- package/dist/components/layout/NuiGrid.svelte.d.ts +14 -0
- package/dist/components/layout/NuiStack.svelte +138 -0
- package/dist/components/layout/NuiStack.svelte.d.ts +26 -0
- package/dist/components/navigation/NuiAccordion.svelte +323 -0
- package/dist/components/navigation/NuiAccordion.svelte.d.ts +38 -0
- package/dist/components/navigation/NuiBreadcrumbs.svelte +133 -0
- package/dist/components/navigation/NuiBreadcrumbs.svelte.d.ts +19 -0
- package/dist/components/navigation/NuiMenu.svelte +596 -0
- package/dist/components/navigation/NuiMenu.svelte.d.ts +28 -0
- package/dist/components/navigation/NuiPagination.svelte +306 -0
- package/dist/components/navigation/NuiPagination.svelte.d.ts +40 -0
- package/dist/components/navigation/NuiSidebar.svelte +186 -0
- package/dist/components/navigation/NuiSidebar.svelte.d.ts +26 -0
- package/dist/components/navigation/NuiTabs.svelte +249 -0
- package/dist/components/navigation/NuiTabs.svelte.d.ts +31 -0
- package/dist/components/overlay/NuiDialog.svelte +118 -0
- package/dist/components/overlay/NuiDialog.svelte.d.ts +34 -0
- package/dist/components/overlay/NuiModal.svelte +992 -0
- package/dist/components/overlay/NuiModal.svelte.d.ts +72 -0
- package/dist/components/overlay/NuiTooltip.svelte +159 -0
- package/dist/components/overlay/NuiTooltip.svelte.d.ts +13 -0
- package/dist/composables/useClickOutside.d.ts +43 -0
- package/dist/composables/useClickOutside.js +73 -0
- package/dist/composables/useFormValidation.d.ts +23 -0
- package/dist/composables/useFormValidation.js +72 -0
- package/dist/composables/useIdle.svelte.d.ts +18 -0
- package/dist/composables/useIdle.svelte.js +144 -0
- package/dist/composables/useKeyboardShortcut.svelte.d.ts +114 -0
- package/dist/composables/useKeyboardShortcut.svelte.js +153 -0
- package/dist/composables/useLocalStorage.svelte.d.ts +22 -0
- package/dist/composables/useLocalStorage.svelte.js +105 -0
- package/dist/composables/useMediaQuery.svelte.d.ts +51 -0
- package/dist/composables/useMediaQuery.svelte.js +80 -0
- package/dist/index.d.ts +46 -0
- package/dist/index.js +42 -0
- package/package.json +58 -3
- package/src/components/actions/NuiButton.svelte +292 -0
- package/src/components/data/NuiBadge.svelte +277 -0
- package/src/components/data/NuiCodeBlock.svelte +256 -0
- package/src/components/data/NuiList.svelte +341 -0
- package/src/components/data/NuiProgressBar.svelte +264 -0
- package/src/components/data/NuiRichText.svelte +260 -0
- package/src/components/data/NuiTable.svelte +470 -0
- package/src/components/data/chart/NuiChart.svelte +384 -0
- package/src/components/data/chart/color.ts +83 -0
- package/src/components/data/chart/index.ts +10 -0
- package/src/components/data/chart/registry.ts +71 -0
- package/src/components/data/chart/renderers/bar.ts +90 -0
- package/src/components/data/chart/renderers/base.ts +32 -0
- package/src/components/data/chart/renderers/line.ts +99 -0
- package/src/components/data/chart/renderers/pie.ts +66 -0
- package/src/components/data/chart/scale.ts +139 -0
- package/src/components/data/chart/types.ts +98 -0
- package/src/components/data/chart/validator.ts +98 -0
- package/src/components/forms/NuiCheckbox.svelte +218 -0
- package/src/components/forms/NuiDatepicker.svelte +755 -0
- package/src/components/forms/NuiFileInput.svelte +1047 -0
- package/src/components/forms/NuiFileInput.types.ts +29 -0
- package/src/components/forms/NuiForm.svelte +391 -0
- package/src/components/forms/NuiInput.svelte +296 -0
- package/src/components/forms/NuiRadioGroup.svelte +238 -0
- package/src/components/forms/NuiSelect.svelte +819 -0
- package/src/components/layout/NuiCard.svelte +314 -0
- package/src/components/layout/NuiCardGrid.svelte +98 -0
- package/src/components/layout/NuiFlex.svelte +121 -0
- package/src/components/layout/NuiGrid.svelte +66 -0
- package/src/components/layout/NuiStack.svelte +138 -0
- package/src/components/navigation/NuiAccordion.svelte +323 -0
- package/src/components/navigation/NuiBreadcrumbs.svelte +133 -0
- package/src/components/navigation/NuiMenu.svelte +596 -0
- package/src/components/navigation/NuiPagination.svelte +306 -0
- package/src/components/navigation/NuiSidebar.svelte +186 -0
- package/src/components/navigation/NuiTabs.svelte +249 -0
- package/src/components/overlay/NuiDialog.svelte +118 -0
- package/src/components/overlay/NuiModal.svelte +992 -0
- package/src/components/overlay/NuiTooltip.svelte +159 -0
- package/src/composables/useClickOutside.ts +132 -0
- package/src/composables/useFormValidation.ts +105 -0
- package/src/composables/useIdle.svelte.ts +191 -0
- package/src/composables/useKeyboardShortcut.svelte.ts +278 -0
- package/src/composables/useLocalStorage.svelte.ts +137 -0
- package/src/composables/useMediaQuery.svelte.ts +102 -0
- package/src/index.ts +66 -0
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
<script lang="ts" module>
|
|
2
|
+
let warnedNoAccessibleName = false;
|
|
3
|
+
</script>
|
|
4
|
+
|
|
5
|
+
<script lang="ts">
|
|
6
|
+
import { DEV } from 'esm-env';
|
|
7
|
+
import type { Snippet } from 'svelte';
|
|
8
|
+
import type { HTMLAttributes } from 'svelte/elements';
|
|
9
|
+
|
|
10
|
+
interface Props extends Omit<HTMLAttributes<HTMLDivElement>, 'title' | 'children'> {
|
|
11
|
+
/** Card header title */
|
|
12
|
+
title?: string;
|
|
13
|
+
/** Optional icon displayed before the title */
|
|
14
|
+
icon?: string;
|
|
15
|
+
/** Card elevation level (1 to 5) */
|
|
16
|
+
elevation?: 1 | 2 | 3 | 4 | 5 | '1' | '2' | '3' | '4' | '5';
|
|
17
|
+
/**
|
|
18
|
+
* Whether the card can be toggled/selected.
|
|
19
|
+
* When true and not `controlled`, clicking toggles `selected` (supports `bind:selected`).
|
|
20
|
+
* For single-select card groups (radio behavior), set `controlled={true}` and update `selected` via `onSelect`.
|
|
21
|
+
*/
|
|
22
|
+
selectable?: boolean;
|
|
23
|
+
/**
|
|
24
|
+
* Whether the card is currently selected.
|
|
25
|
+
* Supports two-way binding via `bind:selected` when `controlled` is false.
|
|
26
|
+
*/
|
|
27
|
+
selected?: boolean;
|
|
28
|
+
/**
|
|
29
|
+
* When true, the parent owns the `selected` state; the card will not mutate `selected` locally upon click.
|
|
30
|
+
* Do not use `bind:selected` together with `controlled={true}`.
|
|
31
|
+
* @default false
|
|
32
|
+
*/
|
|
33
|
+
controlled?: boolean;
|
|
34
|
+
/** Whether the card behaves as a clickable button */
|
|
35
|
+
clickable?: boolean;
|
|
36
|
+
/**
|
|
37
|
+
* Whether the card is disabled.
|
|
38
|
+
* When true, disables the interactive card overlay (if interactive) and marks `actions` and `children` as `inert`.
|
|
39
|
+
* Note: `inert` is applied to `actions` and `children` even on non-interactive cards to suppress descendant interactivity.
|
|
40
|
+
*/
|
|
41
|
+
disabled?: boolean;
|
|
42
|
+
/** Accessible name for the interactive card when there is no title */
|
|
43
|
+
label?: string;
|
|
44
|
+
/** Heading level for the card title (defaults to 3 -> <h3>) */
|
|
45
|
+
headingLevel?: 1 | 2 | 3 | 4 | 5 | 6 | (number & {});
|
|
46
|
+
/** Callback fired when the card is clicked or activated via keyboard. For `selectable` cards, also receives the intended next selection state. */
|
|
47
|
+
onSelect?: (event: MouseEvent | KeyboardEvent, nextSelected?: boolean) => void;
|
|
48
|
+
/** Card body content snippet */
|
|
49
|
+
children?: Snippet;
|
|
50
|
+
/** Header action buttons or badges snippet */
|
|
51
|
+
actions?: Snippet;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const uid = $props.id();
|
|
55
|
+
const titleId = `nui-card-${uid}-title`;
|
|
56
|
+
|
|
57
|
+
let {
|
|
58
|
+
title = '',
|
|
59
|
+
icon = '',
|
|
60
|
+
elevation = 1,
|
|
61
|
+
selectable = false,
|
|
62
|
+
selected = $bindable(false),
|
|
63
|
+
controlled = false,
|
|
64
|
+
clickable = false,
|
|
65
|
+
disabled = false,
|
|
66
|
+
label,
|
|
67
|
+
headingLevel = 3,
|
|
68
|
+
onSelect,
|
|
69
|
+
children,
|
|
70
|
+
actions,
|
|
71
|
+
class: className = '',
|
|
72
|
+
role: roleProp,
|
|
73
|
+
'aria-label': ariaLabel,
|
|
74
|
+
'aria-labelledby': ariaLabelledby,
|
|
75
|
+
...rest
|
|
76
|
+
}: Props = $props();
|
|
77
|
+
|
|
78
|
+
const interactive = $derived(selectable || clickable || Boolean(onSelect));
|
|
79
|
+
const computedAriaLabelledby = $derived(title ? titleId : ariaLabelledby);
|
|
80
|
+
const computedAriaLabel = $derived(
|
|
81
|
+
computedAriaLabelledby ? undefined : (label ?? ariaLabel)
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
const safeHeadingTag = $derived.by(() => {
|
|
85
|
+
const level = Number(headingLevel);
|
|
86
|
+
if (Number.isInteger(level) && level >= 1 && level <= 6) {
|
|
87
|
+
return `h${level}`;
|
|
88
|
+
}
|
|
89
|
+
return 'h3';
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
// Warn once in development mode if an interactive card lacks an accessible name
|
|
93
|
+
$effect(() => {
|
|
94
|
+
if (!DEV || warnedNoAccessibleName) return;
|
|
95
|
+
if (interactive && !computedAriaLabel && !computedAriaLabelledby) {
|
|
96
|
+
warnedNoAccessibleName = true;
|
|
97
|
+
console.warn(
|
|
98
|
+
'[fivem-nui-components] NuiCard: Interactive card must have an accessible name. Provide a `title`, `label`, or `aria-label`.'
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
function handleClick(event: MouseEvent | KeyboardEvent) {
|
|
104
|
+
if (disabled) return;
|
|
105
|
+
if (selectable) {
|
|
106
|
+
const next = !selected;
|
|
107
|
+
if (!controlled) {
|
|
108
|
+
selected = next;
|
|
109
|
+
}
|
|
110
|
+
onSelect?.(event, next);
|
|
111
|
+
} else {
|
|
112
|
+
onSelect?.(event);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function handleKeyDown(event: KeyboardEvent) {
|
|
117
|
+
if (disabled) return;
|
|
118
|
+
if (event.key === 'Enter' || event.key === ' ') {
|
|
119
|
+
event.preventDefault();
|
|
120
|
+
handleClick(event);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
</script>
|
|
124
|
+
|
|
125
|
+
<div
|
|
126
|
+
{...rest}
|
|
127
|
+
role={roleProp ?? (!interactive && (ariaLabel || ariaLabelledby) ? 'group' : undefined)}
|
|
128
|
+
aria-label={!interactive ? ariaLabel : undefined}
|
|
129
|
+
aria-labelledby={!interactive ? ariaLabelledby : undefined}
|
|
130
|
+
class={['nui-card', `nui-card--elevation-${elevation}`, className]}
|
|
131
|
+
class:nui-card--interactive={interactive}
|
|
132
|
+
class:nui-card--selected={selectable && selected}
|
|
133
|
+
class:nui-card--disabled={disabled}
|
|
134
|
+
>
|
|
135
|
+
{#if interactive}
|
|
136
|
+
<button
|
|
137
|
+
type="button"
|
|
138
|
+
class="nui-card-hit"
|
|
139
|
+
{disabled}
|
|
140
|
+
aria-pressed={selectable ? selected : undefined}
|
|
141
|
+
aria-labelledby={computedAriaLabelledby}
|
|
142
|
+
aria-label={computedAriaLabel}
|
|
143
|
+
onclick={handleClick}
|
|
144
|
+
onkeydown={handleKeyDown}
|
|
145
|
+
></button>
|
|
146
|
+
{/if}
|
|
147
|
+
|
|
148
|
+
{#if title || icon || actions}
|
|
149
|
+
<div class="nui-card-header">
|
|
150
|
+
{#if icon}<span class="nui-card-icon" aria-hidden="true">{icon}</span>{/if}
|
|
151
|
+
{#if title}
|
|
152
|
+
<svelte:element this={safeHeadingTag} id={titleId} class="nui-card-title">
|
|
153
|
+
{title}
|
|
154
|
+
</svelte:element>
|
|
155
|
+
{/if}
|
|
156
|
+
{#if actions}
|
|
157
|
+
<div class="nui-card-actions" inert={disabled || undefined}>{@render actions()}</div>
|
|
158
|
+
{/if}
|
|
159
|
+
</div>
|
|
160
|
+
{/if}
|
|
161
|
+
|
|
162
|
+
{#if children}
|
|
163
|
+
<div class="nui-card-body" inert={disabled || undefined}>{@render children()}</div>
|
|
164
|
+
{/if}
|
|
165
|
+
</div>
|
|
166
|
+
|
|
167
|
+
<style>
|
|
168
|
+
.nui-card {
|
|
169
|
+
position: relative;
|
|
170
|
+
background: var(--nui-surface, #18181b);
|
|
171
|
+
color: var(--nui-fg, #fafafa);
|
|
172
|
+
border: 1px solid var(--nui-border, #27272a);
|
|
173
|
+
border-radius: var(--nui-radius, 0.5rem);
|
|
174
|
+
padding: 1rem;
|
|
175
|
+
box-sizing: border-box;
|
|
176
|
+
font-family: inherit;
|
|
177
|
+
box-shadow: var(--nui-card-shadow);
|
|
178
|
+
transition: transform 0.15s ease, border-color 0.15s ease,
|
|
179
|
+
opacity 0.15s ease, box-shadow 0.15s ease;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
.nui-card--elevation-1 { --nui-card-shadow: 0 1px 3px rgb(0 0 0 / 0.12); }
|
|
183
|
+
.nui-card--elevation-2 { --nui-card-shadow: 0 3px 6px rgb(0 0 0 / 0.16); }
|
|
184
|
+
.nui-card--elevation-3 { --nui-card-shadow: 0 10px 20px rgb(0 0 0 / 0.19); }
|
|
185
|
+
.nui-card--elevation-4 { --nui-card-shadow: 0 20px 40px rgb(0 0 0 / 0.23); }
|
|
186
|
+
.nui-card--elevation-5 { --nui-card-shadow: 0 30px 60px rgb(0 0 0 / 0.3); }
|
|
187
|
+
|
|
188
|
+
.nui-card--interactive:not(.nui-card--disabled):hover {
|
|
189
|
+
transform: translateY(-2px);
|
|
190
|
+
border-color: var(--nui-primary, #3b82f6);
|
|
191
|
+
z-index: 2;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
.nui-card:focus-within {
|
|
195
|
+
z-index: 2;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
.nui-card--interactive:not(.nui-card--disabled):active {
|
|
199
|
+
transform: none;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
.nui-card--selected {
|
|
203
|
+
border-color: var(--nui-primary, #3b82f6);
|
|
204
|
+
box-shadow: inset 0 0 0 1px var(--nui-primary, #3b82f6), var(--nui-card-shadow);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
.nui-card--disabled {
|
|
208
|
+
opacity: 0.5;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/* Fallback for environments lacking native inert support */
|
|
212
|
+
.nui-card--disabled .nui-card-actions,
|
|
213
|
+
.nui-card--disabled .nui-card-body {
|
|
214
|
+
pointer-events: none;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
.nui-card-hit {
|
|
218
|
+
position: absolute;
|
|
219
|
+
inset: 0;
|
|
220
|
+
width: 100%;
|
|
221
|
+
margin: 0;
|
|
222
|
+
padding: 0;
|
|
223
|
+
border: 0;
|
|
224
|
+
border-radius: inherit;
|
|
225
|
+
background: transparent;
|
|
226
|
+
appearance: none;
|
|
227
|
+
cursor: pointer;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/* Overlay expands -2px only during hover to eliminate jitter without overlapping adjacent cards at rest */
|
|
231
|
+
.nui-card--interactive:not(.nui-card--disabled):hover .nui-card-hit {
|
|
232
|
+
bottom: -2px;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
.nui-card-hit:disabled {
|
|
236
|
+
cursor: not-allowed;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
.nui-card-hit:focus-visible {
|
|
240
|
+
outline: 2px solid var(--nui-primary, #3b82f6);
|
|
241
|
+
outline-offset: 2px;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
.nui-card-header {
|
|
245
|
+
display: flex;
|
|
246
|
+
align-items: center;
|
|
247
|
+
gap: 0.5rem;
|
|
248
|
+
margin-bottom: 0.75rem;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
.nui-card-header:last-child {
|
|
252
|
+
margin-bottom: 0;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
.nui-card-icon {
|
|
256
|
+
font-size: 1.25rem;
|
|
257
|
+
line-height: 1;
|
|
258
|
+
display: inline-flex;
|
|
259
|
+
align-items: center;
|
|
260
|
+
justify-content: center;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
.nui-card-title {
|
|
264
|
+
margin: 0;
|
|
265
|
+
font-size: 1rem;
|
|
266
|
+
font-weight: 600;
|
|
267
|
+
flex: 1;
|
|
268
|
+
color: inherit;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/* Above overlay so actions, interactive content, and user-marked elements receive direct pointer/keyboard focus */
|
|
272
|
+
.nui-card-actions {
|
|
273
|
+
position: relative;
|
|
274
|
+
z-index: 1;
|
|
275
|
+
display: flex;
|
|
276
|
+
gap: 0.375rem;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
.nui-card-body {
|
|
280
|
+
font-size: 0.875rem;
|
|
281
|
+
color: inherit;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
.nui-card-body :global(:is(
|
|
285
|
+
a,
|
|
286
|
+
button,
|
|
287
|
+
input,
|
|
288
|
+
select,
|
|
289
|
+
textarea,
|
|
290
|
+
label,
|
|
291
|
+
summary,
|
|
292
|
+
[tabindex],
|
|
293
|
+
[role="button"],
|
|
294
|
+
[role="checkbox"],
|
|
295
|
+
[role="radio"],
|
|
296
|
+
[role="switch"],
|
|
297
|
+
[role="link"],
|
|
298
|
+
[role="textbox"],
|
|
299
|
+
[contenteditable],
|
|
300
|
+
.nui-card-interactive
|
|
301
|
+
)) {
|
|
302
|
+
position: relative;
|
|
303
|
+
z-index: 1;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
@media (prefers-reduced-motion: reduce) {
|
|
307
|
+
.nui-card {
|
|
308
|
+
transition: none;
|
|
309
|
+
}
|
|
310
|
+
.nui-card--interactive:hover {
|
|
311
|
+
transform: none;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
</style>
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Snippet } from "svelte";
|
|
3
|
+
import type { HTMLAttributes } from "svelte/elements";
|
|
4
|
+
|
|
5
|
+
interface Props extends HTMLAttributes<HTMLDivElement> {
|
|
6
|
+
/** Max number of columns (responsive) or exact count (fixed) */
|
|
7
|
+
columns?: number | `${number}` | (string & {});
|
|
8
|
+
/**
|
|
9
|
+
* Gap between cards. Numbers are treated as px (e.g. 16 -> '16px').
|
|
10
|
+
* Fixed units (rem, px) are recommended; percentage units in responsive mode may produce
|
|
11
|
+
* variable track minimums because container-relative calc() evaluation depends on outer context.
|
|
12
|
+
*/
|
|
13
|
+
gap?: string | number;
|
|
14
|
+
/** Auto fit columns by `minCardWidth`, capped by `columns` */
|
|
15
|
+
responsive?: boolean;
|
|
16
|
+
/** Minimum card width. Numbers are treated as px */
|
|
17
|
+
minCardWidth?: string | number;
|
|
18
|
+
/** Center cards within their cells */
|
|
19
|
+
center?: boolean;
|
|
20
|
+
children?: Snippet;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
let {
|
|
24
|
+
columns = 3,
|
|
25
|
+
gap = "1rem",
|
|
26
|
+
responsive = true,
|
|
27
|
+
minCardWidth = "250px",
|
|
28
|
+
center = false,
|
|
29
|
+
children,
|
|
30
|
+
class: className = "",
|
|
31
|
+
style,
|
|
32
|
+
...rest
|
|
33
|
+
}: Props = $props();
|
|
34
|
+
|
|
35
|
+
const toCss = (v: string | number | undefined | null, fallback: string) => {
|
|
36
|
+
if (typeof v === "number") return `${v}px`;
|
|
37
|
+
if (typeof v === "string" && v.trim().length > 0) return v.trim();
|
|
38
|
+
return fallback;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const cols = $derived.by(() => {
|
|
42
|
+
if (columns == null) return 3;
|
|
43
|
+
const num = Number(columns);
|
|
44
|
+
if (!Number.isFinite(num)) return 1;
|
|
45
|
+
return Math.max(1, Math.floor(num));
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
const styleVars = $derived(
|
|
49
|
+
`--nui-card-columns:${cols};--nui-card-gap:${toCss(gap, "1rem")};--nui-card-min-width:${toCss(minCardWidth, "250px")};`,
|
|
50
|
+
);
|
|
51
|
+
/**
|
|
52
|
+
* Concatenate extra inline style. Svelte 5 passes style strings here; non-string values are safely ignored.
|
|
53
|
+
*/
|
|
54
|
+
const styleExtra = $derived(typeof style === "string" ? style : "");
|
|
55
|
+
</script>
|
|
56
|
+
|
|
57
|
+
<div
|
|
58
|
+
{...rest}
|
|
59
|
+
class={[
|
|
60
|
+
"nui-card-grid",
|
|
61
|
+
responsive ? "nui-card-grid--responsive" : "nui-card-grid--fixed",
|
|
62
|
+
center && "nui-card-grid--center",
|
|
63
|
+
className,
|
|
64
|
+
]}
|
|
65
|
+
style={styleVars + styleExtra}
|
|
66
|
+
>
|
|
67
|
+
{@render children?.()}
|
|
68
|
+
</div>
|
|
69
|
+
|
|
70
|
+
<style>
|
|
71
|
+
.nui-card-grid {
|
|
72
|
+
display: grid;
|
|
73
|
+
gap: var(--nui-card-gap, 1rem);
|
|
74
|
+
width: 100%;
|
|
75
|
+
font-family: inherit;
|
|
76
|
+
box-sizing: border-box;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
.nui-card-grid--responsive {
|
|
80
|
+
--_gap-total: calc((var(--nui-card-columns) - 1) * var(--nui-card-gap));
|
|
81
|
+
--_min: min(
|
|
82
|
+
100%,
|
|
83
|
+
max(
|
|
84
|
+
var(--nui-card-min-width),
|
|
85
|
+
calc((100% - var(--_gap-total)) / var(--nui-card-columns))
|
|
86
|
+
)
|
|
87
|
+
);
|
|
88
|
+
grid-template-columns: repeat(auto-fit, minmax(var(--_min), 1fr));
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
.nui-card-grid--fixed {
|
|
92
|
+
grid-template-columns: repeat(var(--nui-card-columns), minmax(0, 1fr));
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
.nui-card-grid--center {
|
|
96
|
+
justify-items: center;
|
|
97
|
+
}
|
|
98
|
+
</style>
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Snippet } from "svelte";
|
|
3
|
+
import type { HTMLAttributes } from "svelte/elements";
|
|
4
|
+
|
|
5
|
+
type Direction = "row" | "column" | "row-reverse" | "column-reverse";
|
|
6
|
+
type Justify = "start" | "center" | "end" | "between" | "around" | "evenly";
|
|
7
|
+
type Align = "start" | "center" | "end" | "stretch" | "baseline";
|
|
8
|
+
|
|
9
|
+
interface Props extends HTMLAttributes<HTMLDivElement> {
|
|
10
|
+
/** Main axis direction. Note: `*-reverse` changes visual order only, not DOM/tab order */
|
|
11
|
+
direction?: Direction;
|
|
12
|
+
/** Alignment along the main axis */
|
|
13
|
+
justify?: Justify;
|
|
14
|
+
/** Alignment along the cross axis */
|
|
15
|
+
align?: Align;
|
|
16
|
+
/** Allow items to wrap onto multiple lines */
|
|
17
|
+
wrap?: boolean;
|
|
18
|
+
/** Gap between items. Numbers are treated as px */
|
|
19
|
+
gap?: string | number;
|
|
20
|
+
/** Render as inline-flex */
|
|
21
|
+
inline?: boolean;
|
|
22
|
+
children?: Snippet;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
let {
|
|
26
|
+
direction = "row",
|
|
27
|
+
justify = "start",
|
|
28
|
+
align = "stretch",
|
|
29
|
+
wrap = false,
|
|
30
|
+
gap = "1rem",
|
|
31
|
+
inline = false,
|
|
32
|
+
children,
|
|
33
|
+
class: className = "",
|
|
34
|
+
style,
|
|
35
|
+
...rest
|
|
36
|
+
}: Props = $props();
|
|
37
|
+
|
|
38
|
+
const toCss = (v: string | number) => (typeof v === "number" ? `${v}px` : v);
|
|
39
|
+
const styleVars = $derived(`--nui-flex-gap:${toCss(gap)};`);
|
|
40
|
+
const styleExtra = $derived(typeof style === "string" ? style : "");
|
|
41
|
+
</script>
|
|
42
|
+
|
|
43
|
+
<div
|
|
44
|
+
{...rest}
|
|
45
|
+
class={[
|
|
46
|
+
"nui-flex",
|
|
47
|
+
`nui-flex--${direction}`,
|
|
48
|
+
`nui-flex--justify-${justify}`,
|
|
49
|
+
`nui-flex--align-${align}`,
|
|
50
|
+
wrap && "nui-flex--wrap",
|
|
51
|
+
inline && "nui-flex--inline",
|
|
52
|
+
className,
|
|
53
|
+
]}
|
|
54
|
+
style={styleVars + styleExtra}
|
|
55
|
+
>
|
|
56
|
+
{@render children?.()}
|
|
57
|
+
</div>
|
|
58
|
+
|
|
59
|
+
<style>
|
|
60
|
+
.nui-flex {
|
|
61
|
+
display: flex;
|
|
62
|
+
gap: var(--nui-flex-gap, 1rem);
|
|
63
|
+
font-family: inherit;
|
|
64
|
+
box-sizing: border-box;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
.nui-flex--inline {
|
|
68
|
+
display: inline-flex;
|
|
69
|
+
}
|
|
70
|
+
.nui-flex--wrap {
|
|
71
|
+
flex-wrap: wrap;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
.nui-flex--row {
|
|
75
|
+
flex-direction: row;
|
|
76
|
+
}
|
|
77
|
+
.nui-flex--column {
|
|
78
|
+
flex-direction: column;
|
|
79
|
+
}
|
|
80
|
+
.nui-flex--row-reverse {
|
|
81
|
+
flex-direction: row-reverse;
|
|
82
|
+
}
|
|
83
|
+
.nui-flex--column-reverse {
|
|
84
|
+
flex-direction: column-reverse;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
.nui-flex--justify-start {
|
|
88
|
+
justify-content: flex-start;
|
|
89
|
+
}
|
|
90
|
+
.nui-flex--justify-center {
|
|
91
|
+
justify-content: center;
|
|
92
|
+
}
|
|
93
|
+
.nui-flex--justify-end {
|
|
94
|
+
justify-content: flex-end;
|
|
95
|
+
}
|
|
96
|
+
.nui-flex--justify-between {
|
|
97
|
+
justify-content: space-between;
|
|
98
|
+
}
|
|
99
|
+
.nui-flex--justify-around {
|
|
100
|
+
justify-content: space-around;
|
|
101
|
+
}
|
|
102
|
+
.nui-flex--justify-evenly {
|
|
103
|
+
justify-content: space-evenly;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
.nui-flex--align-start {
|
|
107
|
+
align-items: flex-start;
|
|
108
|
+
}
|
|
109
|
+
.nui-flex--align-center {
|
|
110
|
+
align-items: center;
|
|
111
|
+
}
|
|
112
|
+
.nui-flex--align-end {
|
|
113
|
+
align-items: flex-end;
|
|
114
|
+
}
|
|
115
|
+
.nui-flex--align-stretch {
|
|
116
|
+
align-items: stretch;
|
|
117
|
+
}
|
|
118
|
+
.nui-flex--align-baseline {
|
|
119
|
+
align-items: baseline;
|
|
120
|
+
}
|
|
121
|
+
</style>
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Snippet } from "svelte";
|
|
3
|
+
import type { HTMLAttributes } from "svelte/elements";
|
|
4
|
+
|
|
5
|
+
interface Props extends HTMLAttributes<HTMLDivElement> {
|
|
6
|
+
/** Exact number of equal columns. When omitted, columns auto fit by `minColumnWidth` */
|
|
7
|
+
columns?: number;
|
|
8
|
+
/** Gap between items. Numbers are treated as px */
|
|
9
|
+
gap?: string | number;
|
|
10
|
+
/** Minimum column width in auto mode. Numbers are treated as px */
|
|
11
|
+
minColumnWidth?: string | number;
|
|
12
|
+
children?: Snippet;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
let {
|
|
16
|
+
columns,
|
|
17
|
+
gap = "1rem",
|
|
18
|
+
minColumnWidth = "200px",
|
|
19
|
+
children,
|
|
20
|
+
class: className = "",
|
|
21
|
+
style,
|
|
22
|
+
...rest
|
|
23
|
+
}: Props = $props();
|
|
24
|
+
|
|
25
|
+
const toCss = (v: string | number) => (typeof v === "number" ? `${v}px` : v);
|
|
26
|
+
|
|
27
|
+
const cols = $derived(
|
|
28
|
+
columns !== undefined && Number.isFinite(columns) && columns >= 1
|
|
29
|
+
? Math.floor(columns)
|
|
30
|
+
: undefined,
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
const styleVars = $derived(
|
|
34
|
+
`--nui-grid-gap:${toCss(gap)};--nui-grid-min:${toCss(minColumnWidth)};` +
|
|
35
|
+
(cols ? `--nui-grid-columns:${cols};` : ""),
|
|
36
|
+
);
|
|
37
|
+
const styleExtra = $derived(typeof style === "string" ? style : "");
|
|
38
|
+
</script>
|
|
39
|
+
|
|
40
|
+
<div
|
|
41
|
+
{...rest}
|
|
42
|
+
class={["nui-grid", cols ? "nui-grid--fixed" : "nui-grid--auto", className]}
|
|
43
|
+
style={styleVars + styleExtra}
|
|
44
|
+
>
|
|
45
|
+
{@render children?.()}
|
|
46
|
+
</div>
|
|
47
|
+
|
|
48
|
+
<style>
|
|
49
|
+
.nui-grid {
|
|
50
|
+
display: grid;
|
|
51
|
+
gap: var(--nui-grid-gap, 1rem);
|
|
52
|
+
font-family: inherit;
|
|
53
|
+
box-sizing: border-box;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
.nui-grid--fixed {
|
|
57
|
+
grid-template-columns: repeat(var(--nui-grid-columns), minmax(0, 1fr));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
.nui-grid--auto {
|
|
61
|
+
grid-template-columns: repeat(
|
|
62
|
+
auto-fit,
|
|
63
|
+
minmax(min(100%, var(--nui-grid-min, 200px)), 1fr)
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
</style>
|