@ainsleydev/sveltekit-helper 0.1.4 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -11,12 +11,43 @@ pnpm add @ainsleydev/sveltekit-helper
11
11
  ## Features
12
12
 
13
13
  - **Grid System**: Responsive Container, Row, and Column components with CSS variables
14
+ - **Navigation Components**: Mobile-first Sidebar and Hamburger menu components
14
15
  - **Form Utilities**: Schema generation and error helpers for Zod validation
15
16
  - **Payload CMS Integration**: Ready-to-use components for Payload CMS forms and media
16
17
  - **SCSS with BEM**: All components use SCSS with BEM naming convention
17
18
 
18
19
  ## Grid Components
19
20
 
21
+ ### CSS Variable Customization
22
+
23
+ All Grid components use CSS variables with fallback values, allowing flexible customization:
24
+
25
+ **Override Priority (highest to lowest):**
26
+ 1. Inline styles: `<Container style="--container-padding: 2rem">`
27
+ 2. Page/component-scoped: `.pricing-page { --container-padding: 3rem; }`
28
+ 3. Global: `:root { --container-padding: 2rem; }`
29
+ 4. Component defaults: Defined in each component's `<style>` block
30
+
31
+ **Responsive Variables:**
32
+
33
+ Row and Column components include mobile-specific overrides (< 568px). You can customise responsive behaviour using:
34
+
35
+ ```css
36
+ :root {
37
+ /* Override both desktop and mobile */
38
+ --row-gap: 1.5rem;
39
+
40
+ /* Override mobile only */
41
+ --row-gap-mobile: 0.75rem;
42
+ --col-gap-mobile: 0.75rem;
43
+ }
44
+ ```
45
+
46
+ **Fallback chain on mobile:**
47
+ 1. `--row-gap-mobile` (if set)
48
+ 2. `--row-gap` (if set)
49
+ 3. `0.5rem` (component default)
50
+
20
51
  ### Container
21
52
 
22
53
  Center content horizontally with predefined max-width and support for breakout layouts.
@@ -37,14 +68,28 @@ Center content horizontally with predefined max-width and support for breakout l
37
68
 
38
69
  #### Customisation
39
70
 
40
- Override CSS variables to customise the container:
71
+ Override CSS variables globally from `:root`:
41
72
 
42
73
  ```css
43
- .container {
74
+ /* Global override for ALL containers */
75
+ :root {
44
76
  --container-padding: 2rem;
45
77
  --container-max-width: 1400px;
46
78
  --container-breakout-max-width: 1600px;
47
79
  }
80
+
81
+ /* Page-specific override */
82
+ .pricing-page {
83
+ --container-padding: 3rem;
84
+ }
85
+ ```
86
+
87
+ Or use inline styles for single instances:
88
+
89
+ ```svelte
90
+ <Container style="--container-padding: 2rem">
91
+ <Row>...</Row>
92
+ </Container>
48
93
  ```
49
94
 
50
95
  ### Row
@@ -70,11 +115,21 @@ Flexbox row container with gap management.
70
115
  #### Customisation
71
116
 
72
117
  ```css
73
- .row {
118
+ /* Global override */
119
+ :root {
74
120
  --row-gap: 1.5rem;
121
+ --row-gap-mobile: 0.75rem; /* Optional: mobile-specific gap (< 568px) */
75
122
  }
76
123
  ```
77
124
 
125
+ Or use inline styles:
126
+
127
+ ```svelte
128
+ <Row style="--row-gap: 0.5rem">
129
+ <Column>...</Column>
130
+ </Row>
131
+ ```
132
+
78
133
  ### Column
79
134
 
80
135
  Base column component with customisable gap. Consumers should define their own grid classes in global styles.
@@ -88,8 +143,10 @@ Base column component with customisable gap. Consumers should define their own g
88
143
  #### Customisation
89
144
 
90
145
  ```css
91
- .col {
146
+ /* Global column gap */
147
+ :root {
92
148
  --col-gap: 1.5rem;
149
+ --col-gap-mobile: 0.75rem; /* Optional: mobile-specific gap (< 568px) */
93
150
  }
94
151
 
95
152
  /* Define your own grid classes */
@@ -101,6 +158,128 @@ Base column component with customisable gap. Consumers should define their own g
101
158
  }
102
159
  ```
103
160
 
161
+ ## Navigation Components
162
+
163
+ ### Sidebar
164
+
165
+ Mobile-first sidebar navigation component with toggle and hamburger display modes. Automatically collapses on mobile and remains visible on desktop.
166
+
167
+ ```svelte
168
+ <script>
169
+ import { Sidebar } from '@ainsleydev/sveltekit-helper/components'
170
+ </script>
171
+
172
+ <Sidebar bind:isOpen>
173
+ <nav>
174
+ <a href="/">Home</a>
175
+ <a href="/about">About</a>
176
+ <a href="/contact">Contact</a>
177
+ </nav>
178
+ </Sidebar>
179
+ ```
180
+
181
+ #### Props
182
+
183
+ - `menuLabel?: string` - Label for toggle button (default: 'Menu')
184
+ - `isOpen?: boolean` - Bindable open/closed state
185
+ - `position?: 'left' | 'right'` - Sidebar position (default: 'left')
186
+ - `width?: string` - Sidebar width on mobile (default: '50vw')
187
+ - `top?: number` - Sticky position offset on desktop (default: 160)
188
+ - `closeOnOverlayClick?: boolean` - Close when overlay is clicked (default: true)
189
+ - `overlayOpacity?: number` - Overlay opacity when open (default: 0.3)
190
+ - `toggleStyle?: 'toggle' | 'hamburger'` - Toggle display mode (default: 'toggle')
191
+ - `class?: string` - Additional CSS classes
192
+ - `onOpen?: () => void` - Callback when sidebar opens
193
+ - `onClose?: () => void` - Callback when sidebar closes
194
+ - `onToggle?: (isOpen: boolean) => void` - Callback when sidebar toggles
195
+
196
+ #### Examples
197
+
198
+ With hamburger menu:
199
+
200
+ ```svelte
201
+ <Sidebar toggleStyle="hamburger" bind:isOpen>
202
+ <nav>...</nav>
203
+ </Sidebar>
204
+ ```
205
+
206
+ Right-side with custom width:
207
+
208
+ ```svelte
209
+ <Sidebar position="right" width="300px">
210
+ <nav>...</nav>
211
+ </Sidebar>
212
+ ```
213
+
214
+ #### Customisation
215
+
216
+ Override CSS variables globally from `:root`:
217
+
218
+ ```css
219
+ :root {
220
+ --sidebar-width: 400px;
221
+ --sidebar-min-width: 300px;
222
+ --sidebar-background: #1a1a1a;
223
+ --sidebar-border-colour: rgba(255, 255, 255, 0.2);
224
+ --sidebar-overlay-colour: #000;
225
+ --sidebar-overlay-opacity: 0.5;
226
+
227
+ /* Toggle button */
228
+ --sidebar-toggle-background: #2a2a2a;
229
+ --sidebar-toggle-colour: #fff;
230
+ --sidebar-toggle-padding: 0.5rem 1.5rem;
231
+ --sidebar-toggle-radius: 8px;
232
+ --sidebar-toggle-font-size: 1rem;
233
+
234
+ /* Inner spacing */
235
+ --sidebar-inner-padding: 2rem 2rem 0 2rem;
236
+ }
237
+ ```
238
+
239
+ Or use inline styles:
240
+
241
+ ```svelte
242
+ <Sidebar style="--sidebar-background: #2a2a2a; --sidebar-width: 400px">
243
+ <nav>...</nav>
244
+ </Sidebar>
245
+ ```
246
+
247
+ ### Hamburger
248
+
249
+ Hamburger menu icon with animation for mobile navigation. Uses `svelte-hamburgers` under the hood.
250
+
251
+ ```svelte
252
+ <script>
253
+ import { Hamburger } from '@ainsleydev/sveltekit-helper/components'
254
+
255
+ let isOpen = $state(false)
256
+ </script>
257
+
258
+ <Hamburger bind:isOpen />
259
+ ```
260
+
261
+ #### Props
262
+
263
+ - `isOpen?: boolean` - Bindable open/closed state
264
+ - `gap?: string` - Distance from top/right edges (default: '0.8rem')
265
+ - `class?: string` - Additional CSS classes
266
+ - `ariaLabel?: string` - Accessibility label (default: 'Toggle menu')
267
+ - `onChange?: (isOpen: boolean) => void` - Callback when state changes
268
+
269
+ #### Customisation
270
+
271
+ ```css
272
+ :root {
273
+ --hamburger-gap: 1rem;
274
+ --hamburger-z-index: 10000;
275
+ --hamburger-colour: #fff;
276
+ --hamburger-layer-width: 28px;
277
+ --hamburger-layer-height: 3px;
278
+ --hamburger-layer-spacing: 6px;
279
+ --hamburger-border-radius: 3px;
280
+ }
281
+ ```
282
+
104
283
  ## Form Utilities
105
284
 
106
285
  ### generateFormSchema
@@ -170,10 +349,11 @@ Renders a form dynamically from Payload CMS form builder fields.
170
349
 
171
350
  #### Customisation
172
351
 
173
- Style the form using CSS variables:
352
+ Override CSS variables globally:
174
353
 
175
354
  ```css
176
- .payload-form {
355
+ /* Global form styling */
356
+ :root {
177
357
  --form-gap: 1.5rem;
178
358
  --form-input-padding: 1rem;
179
359
  --form-input-border: 1px solid #e5e7eb;
@@ -181,7 +361,9 @@ Style the form using CSS variables:
181
361
  --form-input-bg: #ffffff;
182
362
  --form-input-text: #111827;
183
363
  --form-error-color: #ef4444;
364
+ --form-error-bg: #fee2e2;
184
365
  --form-success-color: #10b981;
366
+ --form-success-bg: #d1fae5;
185
367
  --form-button-bg: #3b82f6;
186
368
  --form-button-text: #ffffff;
187
369
  --form-button-hover-bg: #2563eb;
@@ -21,13 +21,12 @@ const { ...restProps } = $props();
21
21
  </div>
22
22
 
23
23
  <style>.col {
24
- --col-gap: 1rem;
25
24
  position: relative;
26
25
  width: 100%;
27
- padding-inline: var(--col-gap);
26
+ padding-inline: var(--col-gap, 1rem);
28
27
  }
29
28
  @media (max-width: 568px) {
30
29
  .col {
31
- --col-gap: 0.5rem;
30
+ padding-inline: var(--col-gap-mobile, var(--col-gap, 0.5rem));
32
31
  }
33
32
  }</style>
@@ -13,16 +13,13 @@ const { ...restProps } = $props();
13
13
  </div>
14
14
 
15
15
  <style>.container {
16
- --container-padding: 1rem;
17
- --container-max-width: 1328px;
18
- --container-breakout-max-width: 1500px;
19
16
  --container-breakout-size: calc(
20
- (var(--container-breakout-max-width) - var(--container-max-width)) / 2
17
+ (var(--container-breakout-max-width, 1500px) - var(--container-max-width, 1328px)) / 2
21
18
  );
22
19
  display: grid;
23
20
  width: 100%;
24
21
  position: relative;
25
- grid-template-columns: [full-width-start] minmax(var(--container-padding), 1fr) [breakout-start] minmax(0, var(--container-breakout-size)) [content-start] min(100% - var(--container-padding) * 2, var(--container-max-width)) [content-end] minmax(0, var(--container-breakout-size)) [breakout-end] minmax(var(--container-padding), 1fr) [full-width-end];
22
+ grid-template-columns: [full-width-start] minmax(var(--container-padding, 1rem), 1fr) [breakout-start] minmax(0, var(--container-breakout-size)) [content-start] min(100% - var(--container-padding, 1rem) * 2, var(--container-max-width, 1328px)) [content-end] minmax(0, var(--container-breakout-size)) [breakout-end] minmax(var(--container-padding, 1rem), 1fr) [full-width-end];
26
23
  }
27
24
  .container :global(> *) {
28
25
  grid-column: content;
@@ -32,10 +32,9 @@ const { noGaps = false, ...restProps }: RowProps = $props();
32
32
  </div>
33
33
 
34
34
  <style>.row {
35
- --row-gap: 1rem;
36
35
  display: flex;
37
36
  flex-wrap: wrap;
38
- margin-inline: calc(var(--row-gap) * -1);
37
+ margin-inline: calc(var(--row-gap, 1rem) * -1);
39
38
  }
40
39
  .row--no-gaps {
41
40
  margin-inline: 0;
@@ -46,6 +45,6 @@ const { noGaps = false, ...restProps }: RowProps = $props();
46
45
  }
47
46
  @media (max-width: 568px) {
48
47
  .row {
49
- --row-gap: 0.5rem;
48
+ margin-inline: calc(var(--row-gap-mobile, var(--row-gap, 0.5rem)) * -1);
50
49
  }
51
50
  }</style>
@@ -0,0 +1,62 @@
1
+ <script lang="ts" module>
2
+ import type { HamburgerType as HamburgerTypeLib } from 'svelte-hamburgers';
3
+
4
+ export type HamburgerType = HamburgerTypeLib;
5
+
6
+ export type HamburgerProps = {
7
+ // See: https://github.com/ghostdevv/svelte-hamburgers/blob/main/types.md
8
+ style?: HamburgerType;
9
+ isOpen?: boolean;
10
+ gap?: string;
11
+ class?: string;
12
+ onChange?: (isOpen: boolean) => void;
13
+ };
14
+ </script>
15
+
16
+ <script lang="ts">
17
+ import { Hamburger as SvelteHamburger } from 'svelte-hamburgers';
18
+
19
+ let {
20
+ style = 'spin',
21
+ isOpen = $bindable(false),
22
+ gap = '0.8rem',
23
+ class: className = '',
24
+ onChange
25
+ }: HamburgerProps = $props();
26
+ </script>
27
+
28
+ <!--
29
+ @component
30
+
31
+ Hamburger menu icon with animation for mobile navigation.
32
+ Uses svelte-hamburgers under the hood.
33
+
34
+ @example
35
+ ```svelte
36
+ <Hamburger bind:isOpen />
37
+ ```
38
+
39
+ @example
40
+ ```svelte
41
+ <Hamburger gap="1rem">
42
+ ```
43
+ -->
44
+ <div class="hamburger-wrapper {className}" style="--hamburger-gap: {gap}" aria-label="Toggle Menu">
45
+ <SvelteHamburger
46
+ type={style}
47
+ bind:open={isOpen}
48
+ on:change={() => onChange?.(isOpen)}
49
+ --color="var(--hamburger-colour, var(--colour-base-light))"
50
+ --layer-width="var(--hamburger-layer-width, 24px)"
51
+ --layer-height="var(--hamburger-layer-height, 2px)"
52
+ --layer-spacing="var(--hamburger-layer-spacing, 5px)"
53
+ --border-radius="var(--hamburger-border-radius, 2px)"
54
+ />
55
+ </div>
56
+
57
+ <style>.hamburger-wrapper {
58
+ position: fixed;
59
+ top: var(--hamburger-gap, 0.8rem);
60
+ right: var(--hamburger-gap, 0.8rem);
61
+ z-index: var(--hamburger-z-index, 10000);
62
+ }</style>
@@ -0,0 +1,27 @@
1
+ import type { HamburgerType as HamburgerTypeLib } from 'svelte-hamburgers';
2
+ export type HamburgerType = HamburgerTypeLib;
3
+ export type HamburgerProps = {
4
+ style?: HamburgerType;
5
+ isOpen?: boolean;
6
+ gap?: string;
7
+ class?: string;
8
+ onChange?: (isOpen: boolean) => void;
9
+ };
10
+ /**
11
+ * Hamburger menu icon with animation for mobile navigation.
12
+ * Uses svelte-hamburgers under the hood.
13
+ *
14
+ * @example
15
+ * ```svelte
16
+ * <Hamburger bind:isOpen />
17
+ * ```
18
+ *
19
+ * @example
20
+ * ```svelte
21
+ * <Hamburger gap="1rem">
22
+ * ```
23
+ */
24
+ declare const Hamburger: import("svelte").Component<HamburgerProps, {}, "isOpen">;
25
+ type Hamburger = ReturnType<typeof Hamburger>;
26
+ export default Hamburger;
27
+ //# sourceMappingURL=Hamburger.svelte.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Hamburger.svelte.d.ts","sourceRoot":"","sources":["../../src/components/Hamburger.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,aAAa,IAAI,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAE3E,MAAM,MAAM,aAAa,GAAG,gBAAgB,CAAC;AAE7C,MAAM,MAAM,cAAc,GAAG;IAE5B,KAAK,CAAC,EAAE,aAAa,CAAC;IACtB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,CAAC,MAAM,EAAE,OAAO,KAAK,IAAI,CAAC;CACrC,CAAC;AA4BF;;;;;;;;;;;;;GAaG;AACH,QAAA,MAAM,SAAS,0DAAwC,CAAC;AACxD,KAAK,SAAS,GAAG,UAAU,CAAC,OAAO,SAAS,CAAC,CAAC;AAC9C,eAAe,SAAS,CAAC"}
@@ -0,0 +1,274 @@
1
+ <script lang="ts" module>
2
+ import type { Snippet } from 'svelte';
3
+
4
+ export type SidebarProps = {
5
+ menuLabel?: string;
6
+ children: Snippet;
7
+ isOpen?: boolean;
8
+ position?: 'left' | 'right';
9
+ width?: string;
10
+ top?: number;
11
+ closeOnOverlayClick?: boolean;
12
+ overlayOpacity?: number;
13
+ toggleStyle?: 'toggle' | 'hamburger';
14
+ class?: string;
15
+ onOpen?: () => void;
16
+ onClose?: () => void;
17
+ onToggle?: (isOpen: boolean) => void;
18
+ };
19
+ </script>
20
+
21
+ <script lang="ts">
22
+ import { onMount } from 'svelte';
23
+ import Hamburger from './Hamburger.svelte';
24
+
25
+ let {
26
+ menuLabel = 'Menu',
27
+ children,
28
+ isOpen = $bindable(false),
29
+ position = 'left',
30
+ width = '50vw',
31
+ top = 160,
32
+ closeOnOverlayClick = true,
33
+ overlayOpacity = 0.3,
34
+ toggleStyle = 'toggle',
35
+ class: className = '',
36
+ onOpen,
37
+ onClose,
38
+ onToggle
39
+ }: SidebarProps = $props();
40
+
41
+ const uniqueId = `sidebar-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
42
+ let checkboxRef = $state<HTMLInputElement>();
43
+ let overlayRef = $state<HTMLLabelElement>();
44
+ let contentRef = $state<HTMLDivElement>();
45
+ let previousActiveElement = $state<HTMLElement>();
46
+
47
+ // Sync checkbox with isOpen state.
48
+ $effect(() => {
49
+ if (checkboxRef && checkboxRef.checked !== isOpen) {
50
+ checkboxRef.checked = isOpen;
51
+ }
52
+ });
53
+
54
+ // Watch for changes to isOpen and call callbacks.
55
+ $effect(() => {
56
+ if (isOpen) {
57
+ onOpen?.();
58
+ } else {
59
+ onClose?.();
60
+ }
61
+ onToggle?.(isOpen);
62
+ });
63
+
64
+ // Focus management.
65
+ $effect(() => {
66
+ if (isOpen && contentRef) {
67
+ previousActiveElement = document.activeElement as HTMLElement;
68
+ const firstFocusable = contentRef.querySelector<HTMLElement>(
69
+ 'a, button, input, textarea, select, [tabindex]:not([tabindex="-1"])'
70
+ );
71
+ firstFocusable?.focus();
72
+ } else if (!isOpen && previousActiveElement) {
73
+ previousActiveElement.focus();
74
+ previousActiveElement = undefined;
75
+ }
76
+ });
77
+
78
+ onMount(() => {
79
+ // Capture refs in local variables to ensure proper cleanup.
80
+ const overlay = overlayRef;
81
+ const checkbox = checkboxRef;
82
+
83
+ if (!overlay || !checkbox) return;
84
+
85
+ const handleOverlayClick = (e: Event) => {
86
+ if (!closeOnOverlayClick) return;
87
+ e.preventDefault();
88
+ checkbox.checked = false;
89
+ isOpen = false;
90
+ };
91
+
92
+ const handleCheckboxChange = () => {
93
+ isOpen = checkbox.checked;
94
+ };
95
+
96
+ const handleKeydown = (e: KeyboardEvent) => {
97
+ if (e.key === 'Escape' && isOpen) {
98
+ e.preventDefault();
99
+ checkbox.checked = false;
100
+ isOpen = false;
101
+ }
102
+ };
103
+
104
+ overlay.addEventListener('click', handleOverlayClick);
105
+ checkbox.addEventListener('change', handleCheckboxChange);
106
+ document.addEventListener('keydown', handleKeydown);
107
+
108
+ return () => {
109
+ overlay.removeEventListener('click', handleOverlayClick);
110
+ checkbox.removeEventListener('change', handleCheckboxChange);
111
+ document.removeEventListener('keydown', handleKeydown);
112
+ };
113
+ });
114
+ </script>
115
+
116
+ <!--
117
+ @component
118
+
119
+ Mobile-first sidebar navigation component with toggle and hamburger options.
120
+ Automatically collapses on mobile and remains visible on desktop.
121
+
122
+ @example
123
+ ```svelte
124
+ <Sidebar bind:isOpen>
125
+ <nav>
126
+ <a href="/">Home</a>
127
+ <a href="/about">About</a>
128
+ </nav>
129
+ </Sidebar>
130
+ ```
131
+
132
+ @example
133
+ ```svelte
134
+ <Sidebar toggleStyle="hamburger" position="right" width="300px">
135
+ <nav>...</nav>
136
+ </Sidebar>
137
+ ```
138
+ -->
139
+ <aside
140
+ class="sidebar sidebar--{toggleStyle} sidebar--{position} {className}"
141
+ style="--sidebar-width: {width}; --sidebar-top: {top}px; --sidebar-overlay-opacity: {overlayOpacity}"
142
+ >
143
+ <!-- Click Logic -->
144
+ <input
145
+ bind:this={checkboxRef}
146
+ type="checkbox"
147
+ class="sidebar__checkbox"
148
+ id={uniqueId}
149
+ aria-label={menuLabel}
150
+ />
151
+ <label bind:this={overlayRef} for={uniqueId} class="sidebar__overlay"></label>
152
+ <!-- Hamburger -->
153
+ {#if toggleStyle === 'hamburger'}
154
+ <Hamburger bind:isOpen />
155
+ {/if}
156
+ <!-- Content -->
157
+ <div bind:this={contentRef} class="sidebar__content" role="navigation" aria-label={menuLabel}>
158
+ {#if toggleStyle === 'toggle'}
159
+ <label for={uniqueId} class="sidebar__toggle">
160
+ {menuLabel}
161
+ </label>
162
+ {/if}
163
+ <div class="sidebar__inner">
164
+ {@render children()}
165
+ </div>
166
+ </div>
167
+ </aside>
168
+
169
+ <style>.sidebar__toggle {
170
+ position: absolute;
171
+ display: none;
172
+ bottom: 0;
173
+ right: 1px;
174
+ background-color: var(--sidebar-toggle-background, var(--colour-base-black));
175
+ color: var(--sidebar-toggle-colour, var(--colour-base-light));
176
+ padding: var(--sidebar-toggle-padding, 0.25rem 1.5rem);
177
+ border-top-right-radius: var(--sidebar-toggle-radius, 0.375rem);
178
+ border-top-left-radius: var(--sidebar-toggle-radius, 0.375rem);
179
+ font-size: var(--sidebar-toggle-font-size, 0.9rem);
180
+ transform: rotate(90deg) translate(0%, -100%);
181
+ transform-origin: right top;
182
+ cursor: pointer;
183
+ user-select: none;
184
+ transition: box-shadow 200ms linear;
185
+ border: 1px solid var(--sidebar-border-colour, rgba(255, 255, 255, 0.1));
186
+ }
187
+ .sidebar__toggle::before {
188
+ content: "";
189
+ position: absolute;
190
+ top: calc(90% + 2px);
191
+ left: 1px;
192
+ width: calc(100% - 2px);
193
+ height: 10%;
194
+ background: var(--sidebar-toggle-background, var(--colour-base-black));
195
+ }
196
+ .sidebar__overlay {
197
+ position: fixed;
198
+ top: 0;
199
+ left: 0;
200
+ width: 100%;
201
+ height: 100%;
202
+ background-color: var(--sidebar-overlay-colour, var(--colour-grey-900));
203
+ z-index: -100;
204
+ opacity: 0;
205
+ transition: opacity 400ms ease, z-index 400ms step-end;
206
+ }
207
+ .sidebar__checkbox {
208
+ position: fixed;
209
+ top: 0;
210
+ display: none;
211
+ }
212
+ .sidebar__checkbox:checked ~ .sidebar__content {
213
+ translate: 0;
214
+ z-index: 9999999;
215
+ }
216
+ .sidebar__checkbox:checked ~ .sidebar__content .sidebar__toggle {
217
+ box-shadow: none;
218
+ }
219
+ .sidebar__checkbox:checked ~ .sidebar__overlay {
220
+ transition: opacity 600ms ease, z-index 600ms step-start;
221
+ opacity: var(--sidebar-overlay-opacity, 0.3);
222
+ z-index: 999999;
223
+ }
224
+ @media (max-width: 1023px) {
225
+ .sidebar__content {
226
+ position: fixed;
227
+ display: grid;
228
+ top: 0;
229
+ height: 100%;
230
+ width: var(--sidebar-width, 50vw);
231
+ min-width: var(--sidebar-min-width, 270px);
232
+ background-color: var(--sidebar-background, var(--colour-base-black));
233
+ border-color: var(--sidebar-border-colour, rgba(255, 255, 255, 0.1));
234
+ z-index: 1000;
235
+ transition: translate 600ms cubic-bezier(0.1, 0.7, 0.1, 1);
236
+ }
237
+ .sidebar__inner {
238
+ overflow: auto;
239
+ display: flex;
240
+ flex-direction: column;
241
+ padding: var(--sidebar-inner-padding, 2rem 1.8rem 0 1.8rem);
242
+ }
243
+ .sidebar__toggle {
244
+ display: flex;
245
+ }
246
+ }
247
+ @media (min-width: 1024px) {
248
+ .sidebar {
249
+ position: sticky;
250
+ top: var(--sidebar-top, 160px);
251
+ }
252
+ .sidebar__overlay {
253
+ display: none;
254
+ }
255
+ }
256
+ @media (max-width: 1023px) {
257
+ .sidebar--left .sidebar__content {
258
+ left: 0;
259
+ border-right-style: solid;
260
+ border-right-width: 1px;
261
+ translate: -100%;
262
+ }
263
+ }
264
+ @media (max-width: 1023px) {
265
+ .sidebar--right .sidebar__content {
266
+ right: 0;
267
+ border-left-style: solid;
268
+ border-left-width: 1px;
269
+ translate: 100%;
270
+ }
271
+ }
272
+ .sidebar--hamburger .sidebar__toggle {
273
+ display: none;
274
+ }</style>
@@ -0,0 +1,41 @@
1
+ import type { Snippet } from 'svelte';
2
+ export type SidebarProps = {
3
+ menuLabel?: string;
4
+ children: Snippet;
5
+ isOpen?: boolean;
6
+ position?: 'left' | 'right';
7
+ width?: string;
8
+ top?: number;
9
+ closeOnOverlayClick?: boolean;
10
+ overlayOpacity?: number;
11
+ toggleStyle?: 'toggle' | 'hamburger';
12
+ class?: string;
13
+ onOpen?: () => void;
14
+ onClose?: () => void;
15
+ onToggle?: (isOpen: boolean) => void;
16
+ };
17
+ /**
18
+ * Mobile-first sidebar navigation component with toggle and hamburger options.
19
+ * Automatically collapses on mobile and remains visible on desktop.
20
+ *
21
+ * @example
22
+ * ```svelte
23
+ * <Sidebar bind:isOpen>
24
+ * <nav>
25
+ * <a href="/">Home</a>
26
+ * <a href="/about">About</a>
27
+ * </nav>
28
+ * </Sidebar>
29
+ * ```
30
+ *
31
+ * @example
32
+ * ```svelte
33
+ * <Sidebar toggleStyle="hamburger" position="right" width="300px">
34
+ * <nav>...</nav>
35
+ * </Sidebar>
36
+ * ```
37
+ */
38
+ declare const Sidebar: import("svelte").Component<SidebarProps, {}, "isOpen">;
39
+ type Sidebar = ReturnType<typeof Sidebar>;
40
+ export default Sidebar;
41
+ //# sourceMappingURL=Sidebar.svelte.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Sidebar.svelte.d.ts","sourceRoot":"","sources":["../../src/components/Sidebar.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;AAEtC,MAAM,MAAM,YAAY,GAAG;IAC1B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,OAAO,CAAC;IAClB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;IAC5B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,mBAAmB,CAAC,EAAE,OAAO,CAAC;IAC9B,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,WAAW,CAAC,EAAE,QAAQ,GAAG,WAAW,CAAC;IACrC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,IAAI,CAAC;IACpB,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;IACrB,QAAQ,CAAC,EAAE,CAAC,MAAM,EAAE,OAAO,KAAK,IAAI,CAAC;CACrC,CAAC;AAiIF;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,QAAA,MAAM,OAAO,wDAAwC,CAAC;AACtD,KAAK,OAAO,GAAG,UAAU,CAAC,OAAO,OAAO,CAAC,CAAC;AAC1C,eAAe,OAAO,CAAC"}
@@ -0,0 +1,3 @@
1
+ export { default as Sidebar } from './Sidebar.svelte';
2
+ export { default as Hamburger } from './Hamburger.svelte';
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/components/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,IAAI,OAAO,EAAE,MAAM,kBAAkB,CAAC;AACtD,OAAO,EAAE,OAAO,IAAI,SAAS,EAAE,MAAM,oBAAoB,CAAC"}
@@ -0,0 +1,2 @@
1
+ export { default as Sidebar } from './Sidebar.svelte';
2
+ export { default as Hamburger } from './Hamburger.svelte';
@@ -186,23 +186,9 @@ async function handleSubmit(event: SubmitEvent) {
186
186
  </form>
187
187
 
188
188
  <style>.payload-form {
189
- --form-gap: 1rem;
190
- --form-input-padding: 0.75rem;
191
- --form-input-border: 1px solid #d1d5db;
192
- --form-input-border-radius: 0.375rem;
193
- --form-input-bg: #ffffff;
194
- --form-input-text: #111827;
195
- --form-error-color: #ef4444;
196
- --form-error-bg: #fee2e2;
197
- --form-success-color: #10b981;
198
- --form-success-bg: #d1fae5;
199
- --form-button-bg: #3b82f6;
200
- --form-button-text: #ffffff;
201
- --form-button-hover-bg: #2563eb;
202
- --form-button-disabled-bg: #9ca3af;
203
189
  display: flex;
204
190
  flex-direction: column;
205
- gap: var(--form-gap);
191
+ gap: var(--form-gap, 1rem);
206
192
  }
207
193
  .payload-form__group {
208
194
  display: flex;
@@ -212,31 +198,31 @@ async function handleSubmit(event: SubmitEvent) {
212
198
  .payload-form__label {
213
199
  font-size: 0.875rem;
214
200
  font-weight: 500;
215
- color: var(--form-input-text);
201
+ color: var(--form-input-text, #111827);
216
202
  }
217
203
  .payload-form__label--checkbox {
218
204
  font-weight: 400;
219
205
  cursor: pointer;
220
206
  }
221
207
  .payload-form__required {
222
- color: var(--form-error-color);
208
+ color: var(--form-error-color, #ef4444);
223
209
  margin-left: 0.25rem;
224
210
  }
225
211
  .payload-form__input, .payload-form__textarea {
226
- padding: var(--form-input-padding);
227
- border: var(--form-input-border);
228
- border-radius: var(--form-input-border-radius);
229
- background: var(--form-input-bg);
230
- color: var(--form-input-text);
212
+ padding: var(--form-input-padding, 0.75rem);
213
+ border: var(--form-input-border, 1px solid #d1d5db);
214
+ border-radius: var(--form-input-border-radius, 0.375rem);
215
+ background: var(--form-input-bg, #ffffff);
216
+ color: var(--form-input-text, #111827);
231
217
  font-size: 1rem;
232
218
  font-family: inherit;
233
219
  }
234
220
  .payload-form__input:focus, .payload-form__textarea:focus {
235
- outline: 2px solid var(--form-button-bg);
221
+ outline: 2px solid var(--form-button-bg, #3b82f6);
236
222
  outline-offset: 2px;
237
223
  }
238
224
  .payload-form__input--error, .payload-form__textarea--error {
239
- border-color: var(--form-error-color);
225
+ border-color: var(--form-error-color, #ef4444);
240
226
  }
241
227
  .payload-form__textarea {
242
228
  resize: vertical;
@@ -255,28 +241,28 @@ async function handleSubmit(event: SubmitEvent) {
255
241
  }
256
242
  .payload-form__error {
257
243
  font-size: 0.875rem;
258
- color: var(--form-error-color);
244
+ color: var(--form-error-color, #ef4444);
259
245
  }
260
246
  .payload-form__success {
261
247
  padding: 1rem;
262
- background-color: var(--form-success-bg);
263
- color: var(--form-success-color);
264
- border-radius: var(--form-input-border-radius);
248
+ background-color: var(--form-success-bg, #d1fae5);
249
+ color: var(--form-success-color, #10b981);
250
+ border-radius: var(--form-input-border-radius, 0.375rem);
265
251
  font-weight: 500;
266
252
  }
267
253
  .payload-form__alert {
268
254
  padding: 1rem;
269
- background-color: var(--form-error-bg);
270
- color: var(--form-error-color);
271
- border-radius: var(--form-input-border-radius);
255
+ background-color: var(--form-error-bg, #fee2e2);
256
+ color: var(--form-error-color, #ef4444);
257
+ border-radius: var(--form-input-border-radius, 0.375rem);
272
258
  font-weight: 500;
273
259
  }
274
260
  .payload-form__submit {
275
- padding: var(--form-input-padding) 1.5rem;
276
- background-color: var(--form-button-bg);
277
- color: var(--form-button-text);
261
+ padding: var(--form-input-padding, 0.75rem) 1.5rem;
262
+ background-color: var(--form-button-bg, #3b82f6);
263
+ color: var(--form-button-text, #ffffff);
278
264
  border: none;
279
- border-radius: var(--form-input-border-radius);
265
+ border-radius: var(--form-input-border-radius, 0.375rem);
280
266
  font-size: 1rem;
281
267
  font-weight: 500;
282
268
  cursor: pointer;
@@ -287,10 +273,10 @@ async function handleSubmit(event: SubmitEvent) {
287
273
  transition: background-color 0.2s;
288
274
  }
289
275
  .payload-form__submit:hover:not(:disabled) {
290
- background-color: var(--form-button-hover-bg);
276
+ background-color: var(--form-button-hover-bg, #2563eb);
291
277
  }
292
278
  .payload-form__submit:disabled {
293
- background-color: var(--form-button-disabled-bg);
279
+ background-color: var(--form-button-disabled-bg, #9ca3af);
294
280
  cursor: not-allowed;
295
281
  }
296
282
  .payload-form__loader {
package/dist/index.d.ts CHANGED
@@ -4,6 +4,7 @@
4
4
  * SvelteKit utilities, components and helpers for ainsley.dev builds.
5
5
  * Provides form utilities, grid components, and Payload CMS integrations.
6
6
  */
7
+ export * from './components/index.js';
7
8
  export * from './components/Grid/index.js';
8
9
  export * from './components/payload/index.js';
9
10
  export * from './utils/forms/index.js';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,cAAc,4BAA4B,CAAC;AAC3C,cAAc,+BAA+B,CAAC;AAC9C,cAAc,wBAAwB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,cAAc,uBAAuB,CAAC;AACtC,cAAc,4BAA4B,CAAC;AAC3C,cAAc,+BAA+B,CAAC;AAC9C,cAAc,wBAAwB,CAAC"}
package/dist/index.js CHANGED
@@ -5,6 +5,7 @@
5
5
  * Provides form utilities, grid components, and Payload CMS integrations.
6
6
  */
7
7
  // Re-export all exports from submodules
8
+ export * from './components/index.js';
8
9
  export * from './components/Grid/index.js';
9
10
  export * from './components/payload/index.js';
10
11
  export * from './utils/forms/index.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ainsleydev/sveltekit-helper",
3
- "version": "0.1.4",
3
+ "version": "0.2.0",
4
4
  "description": "SvelteKit utilities, components and helpers for ainsley.dev builds",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -29,6 +29,11 @@
29
29
  "svelte": "./dist/index.js",
30
30
  "default": "./dist/index.js"
31
31
  },
32
+ "./components": {
33
+ "types": "./dist/components/index.d.ts",
34
+ "svelte": "./dist/components/index.js",
35
+ "default": "./dist/components/index.js"
36
+ },
32
37
  "./components/Grid": {
33
38
  "types": "./dist/components/Grid/index.d.ts",
34
39
  "svelte": "./dist/components/Grid/index.js",
@@ -67,7 +72,9 @@
67
72
  "optional": true
68
73
  }
69
74
  },
70
- "dependencies": {},
75
+ "dependencies": {
76
+ "svelte-hamburgers": "^4.1.0"
77
+ },
71
78
  "devDependencies": {
72
79
  "@sveltejs/kit": "^2.0.0",
73
80
  "@sveltejs/package": "^2.0.0",