@hyphen/hyphen-components 2.24.0 → 2.25.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.
@@ -0,0 +1,629 @@
1
+ import React, {
2
+ useCallback,
3
+ useMemo,
4
+ useState,
5
+ useEffect,
6
+ forwardRef,
7
+ } from 'react';
8
+ import { Slot } from '@radix-ui/react-slot';
9
+ import classNames from 'classnames';
10
+ import { Button } from '../Button/Button';
11
+ import { Drawer } from '../Drawer/Drawer';
12
+ import { useIsMobile } from '../../hooks/useIsMobile/useIsMobile';
13
+ import { Box } from '../Box/Box';
14
+ import { IconName } from 'src/types';
15
+ import { Icon } from '../Icon/Icon';
16
+
17
+ const SIDEBAR_COOKIE_NAME = 'sidebar:state';
18
+ const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7;
19
+ const SIDEBAR_WIDTH = '16rem';
20
+ const SIDEBAR_WIDTH_ICON = '3rem';
21
+ const SIDEBAR_KEYBOARD_SHORTCUT = '[';
22
+
23
+ interface SidebarContextProps {
24
+ state: 'expanded' | 'collapsed';
25
+ open: boolean;
26
+ setOpen: (open: boolean) => void;
27
+ openMobile: boolean;
28
+ setOpenMobile: (open: boolean) => void;
29
+ isMobile: boolean;
30
+ toggleSidebar: () => void;
31
+ }
32
+
33
+ const SidebarContext = React.createContext<SidebarContextProps | null>(null);
34
+
35
+ function useSidebar() {
36
+ const context = React.useContext(SidebarContext);
37
+ if (!context) {
38
+ throw new Error('useSidebar must be used within a SidebarProvider.');
39
+ }
40
+ return context;
41
+ }
42
+
43
+ const SidebarProvider = forwardRef<
44
+ HTMLDivElement,
45
+ React.ComponentProps<'div'> & {
46
+ defaultOpen?: boolean;
47
+ open?: boolean;
48
+ onOpenChange?: (open: boolean) => void;
49
+ }
50
+ >(
51
+ (
52
+ {
53
+ defaultOpen = true,
54
+ open: openProp,
55
+ onOpenChange: setOpenProp,
56
+ className,
57
+ style,
58
+ children,
59
+ ...props
60
+ },
61
+ ref
62
+ ) => {
63
+ const isMobile = useIsMobile();
64
+ const [openMobile, setOpenMobile] = useState(openProp ?? defaultOpen);
65
+
66
+ // Manages sidebar open state with a fallback to internal state when openProp is not provided
67
+ const [_open, _setOpen] = useState(openProp ?? defaultOpen);
68
+ const open = openProp ?? _open;
69
+
70
+ const setOpen = useCallback(
71
+ (value: boolean | ((value: boolean) => boolean)) => {
72
+ const newOpenState = typeof value === 'function' ? value(open) : value;
73
+
74
+ if (newOpenState !== open) {
75
+ if (setOpenProp) {
76
+ setOpenProp(newOpenState);
77
+ } else {
78
+ _setOpen(newOpenState);
79
+ }
80
+
81
+ // Set cookie only if state changes
82
+ document.cookie = `${SIDEBAR_COOKIE_NAME}=${newOpenState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`;
83
+ }
84
+ },
85
+ [setOpenProp, open]
86
+ );
87
+
88
+ // Toggle sidebar based on screen type
89
+ const toggleSidebar = useCallback(() => {
90
+ isMobile ? setOpenMobile((open) => !open) : setOpen((open) => !open);
91
+ }, [isMobile, setOpen, setOpenMobile]);
92
+
93
+ // Keydown event handler for toggling sidebar
94
+ useEffect(() => {
95
+ const handleKeyDown = (event: KeyboardEvent) => {
96
+ if (
97
+ event.key === SIDEBAR_KEYBOARD_SHORTCUT &&
98
+ (event.metaKey || event.ctrlKey)
99
+ ) {
100
+ event.preventDefault();
101
+ toggleSidebar();
102
+ }
103
+ };
104
+
105
+ window.addEventListener('keydown', handleKeyDown);
106
+ return () => window.removeEventListener('keydown', handleKeyDown);
107
+ }, [toggleSidebar]);
108
+
109
+ // Update open state when `isMobile` changes
110
+ useEffect(() => {
111
+ if (isMobile && open) {
112
+ setOpenMobile(open);
113
+ }
114
+ }, [isMobile, open]);
115
+
116
+ // Assign state for data attributes
117
+ const state = open ? 'expanded' : 'collapsed';
118
+
119
+ const contextValue = useMemo<SidebarContextProps>(
120
+ () => ({
121
+ state,
122
+ open,
123
+ setOpen,
124
+ isMobile,
125
+ openMobile,
126
+ setOpenMobile,
127
+ toggleSidebar,
128
+ }),
129
+ [state, open, setOpen, isMobile, openMobile, setOpenMobile, toggleSidebar]
130
+ );
131
+
132
+ return (
133
+ <SidebarContext.Provider value={contextValue}>
134
+ <div
135
+ style={
136
+ {
137
+ '--sidebar-width': SIDEBAR_WIDTH,
138
+ '--sidebar-width-icon': SIDEBAR_WIDTH_ICON,
139
+ minBlockSize: '100svh',
140
+ ...style,
141
+ } as React.CSSProperties
142
+ }
143
+ className={classNames(
144
+ 'display-flex w-100 background-color-secondary',
145
+ className
146
+ )}
147
+ ref={ref}
148
+ {...props}
149
+ >
150
+ {children}
151
+ </div>
152
+ </SidebarContext.Provider>
153
+ );
154
+ }
155
+ );
156
+ SidebarProvider.displayName = 'SidebarProvider';
157
+
158
+ const Sidebar = React.forwardRef<
159
+ HTMLDivElement,
160
+ React.ComponentProps<'div'> & {
161
+ side?: 'left'; // no right sidebar yet
162
+ collapsible?: 'offcanvas' | 'icon' | 'none';
163
+ }
164
+ >(
165
+ (
166
+ { side = 'left', collapsible = 'offcanvas', className, children, ...props },
167
+ ref
168
+ ) => {
169
+ const { isMobile, state, openMobile, setOpenMobile } = useSidebar();
170
+
171
+ if (isMobile) {
172
+ return (
173
+ <Drawer
174
+ isOpen={openMobile}
175
+ onDismiss={() => setOpenMobile(false)}
176
+ placement={side}
177
+ >
178
+ <Box data-sidebar="sidebar" data-mobile="true">
179
+ {children}
180
+ </Box>
181
+ </Drawer>
182
+ );
183
+ }
184
+
185
+ if (collapsible === 'none') {
186
+ return (
187
+ <div
188
+ className={classNames(
189
+ 'display-flex h-100 font-size-xs flex-direction-column background-color-secondary font-color-base',
190
+ className
191
+ )}
192
+ style={{
193
+ width: 'var(--sidebar-width)',
194
+ }}
195
+ ref={ref}
196
+ {...props}
197
+ >
198
+ {children}
199
+ </div>
200
+ );
201
+ }
202
+
203
+ return (
204
+ <Box
205
+ ref={ref}
206
+ background="primary"
207
+ display={{ base: 'none', desktop: 'block' }}
208
+ color="base"
209
+ fontSize="sm"
210
+ data-state={state}
211
+ data-collapsible={collapsible}
212
+ data-side={side}
213
+ >
214
+ <div
215
+ style={{
216
+ animationTimingFunction: 'var(--sidebar-transition-timing, linear)',
217
+ transitionTimingFunction:
218
+ 'var(--sidebar-transition-timing, linear)',
219
+ transitionDuration: 'var(--sidebar-transition-duration, 200ms)',
220
+ animationDuration: 'var(--sidebar-transition-duration, 200ms)',
221
+ transitionProperty: 'width',
222
+ width: state === 'collapsed' ? '0' : 'var(--sidebar-width)',
223
+ height: '100svh',
224
+ }}
225
+ className={classNames('position-relative', className)}
226
+ />
227
+ <div
228
+ className={classNames(
229
+ 'position-fixed display-none display-flex-desktop ',
230
+ className
231
+ )}
232
+ style={{
233
+ left: state === 'expanded' ? '0' : 'calc(var(--sidebar-width)*-1)',
234
+ top: '0',
235
+ bottom: '0',
236
+ zIndex: 'var(--size-z-index-drawer)',
237
+ transitionProperty: 'left, right, width',
238
+ width: 'var(--sidebar-width)',
239
+ height: '100svh',
240
+ }}
241
+ {...props}
242
+ >
243
+ <div
244
+ data-sidebar="sidebar"
245
+ className="display-flex h-100 w-100 flex-direction-column background-color-secondary font-color-base"
246
+ >
247
+ {children}
248
+ </div>
249
+ </div>
250
+ </Box>
251
+ );
252
+ }
253
+ );
254
+ Sidebar.displayName = 'Sidebar';
255
+
256
+ const SidebarTrigger = React.forwardRef<
257
+ React.ElementRef<typeof Button>,
258
+ React.ComponentProps<typeof Button>
259
+ >(({ className, onClick, ...props }, ref) => {
260
+ const { toggleSidebar } = useSidebar();
261
+
262
+ return (
263
+ <Button
264
+ ref={ref}
265
+ data-sidebar="trigger"
266
+ variant="tertiary"
267
+ size="sm"
268
+ iconPrefix="dock-left"
269
+ className={className}
270
+ onClick={(event) => {
271
+ onClick?.(event);
272
+ toggleSidebar();
273
+ }}
274
+ aria-label="toggle sidebar"
275
+ {...props}
276
+ />
277
+ );
278
+ });
279
+ SidebarTrigger.displayName = 'SidebarTrigger';
280
+
281
+ const SidebarInset = React.forwardRef<
282
+ HTMLDivElement,
283
+ React.ComponentProps<'main'>
284
+ >(({ className, ...props }, ref) => {
285
+ return (
286
+ <main
287
+ ref={ref}
288
+ className={classNames(
289
+ 'display-flex flex-auto flex-direction-column g-lg align-items-flex-start p-lg background-color-secondary',
290
+ className
291
+ )}
292
+ {...props}
293
+ />
294
+ );
295
+ });
296
+ SidebarInset.displayName = 'SidebarInset';
297
+
298
+ const SidebarHeader = React.forwardRef<
299
+ HTMLDivElement,
300
+ React.ComponentProps<'div'>
301
+ >(({ className, ...props }, ref) => {
302
+ return (
303
+ <div
304
+ ref={ref}
305
+ data-sidebar="header"
306
+ className={classNames(
307
+ 'display-flex g-sm p-v-md p-h-md p-right-0-desktop',
308
+ className
309
+ )}
310
+ {...props}
311
+ />
312
+ );
313
+ });
314
+ SidebarHeader.displayName = 'SidebarHeader';
315
+
316
+ const SidebarFooter = React.forwardRef<
317
+ HTMLDivElement,
318
+ React.ComponentProps<'div'>
319
+ >(({ className, ...props }, ref) => {
320
+ return (
321
+ <div
322
+ ref={ref}
323
+ data-sidebar="footer"
324
+ className={classNames(
325
+ 'display-flex g-sm p-v-md p-h-md p-right-0-desktop',
326
+ className
327
+ )}
328
+ {...props}
329
+ />
330
+ );
331
+ });
332
+ SidebarFooter.displayName = 'SidebarFooter';
333
+
334
+ const SidebarContent = React.forwardRef<
335
+ HTMLDivElement,
336
+ React.ComponentProps<'div'>
337
+ >(({ className, ...props }, ref) => {
338
+ return (
339
+ <div
340
+ ref={ref}
341
+ data-sidebar="content"
342
+ className={classNames(
343
+ 'display-flex flex-direction-column g-xl minh-0 flex-auto overflow-auto',
344
+ className
345
+ )}
346
+ {...props}
347
+ />
348
+ );
349
+ });
350
+ SidebarContent.displayName = 'SidebarContent';
351
+
352
+ const SidebarMenu = React.forwardRef<
353
+ HTMLUListElement,
354
+ React.ComponentProps<'ul'>
355
+ >(({ className, ...props }, ref) => (
356
+ <ul
357
+ ref={ref}
358
+ data-sidebar="menu"
359
+ className={classNames(
360
+ 'display-flex flex-direction-column w-100 minw-0 g-xs p-0 m-0',
361
+ className
362
+ )}
363
+ style={{
364
+ listStyle: 'none',
365
+ }}
366
+ {...props}
367
+ />
368
+ ));
369
+ SidebarMenu.displayName = 'SidebarMenu';
370
+
371
+ const SidebarMenuItem = React.forwardRef<
372
+ HTMLLIElement,
373
+ React.ComponentProps<'li'>
374
+ >(({ className, ...props }, ref) => (
375
+ <li
376
+ ref={ref}
377
+ data-sidebar="menu-item"
378
+ className={classNames('font-size-sm position-relative', className)}
379
+ {...props}
380
+ />
381
+ ));
382
+ SidebarMenuItem.displayName = 'SidebarMenuItem';
383
+
384
+ const SidebarMenuButton = React.forwardRef<
385
+ HTMLButtonElement,
386
+ React.ComponentProps<'button'> & {
387
+ asChild?: boolean;
388
+ isActive?: boolean;
389
+ icon?: IconName;
390
+ }
391
+ >(({ asChild = false, isActive = false, icon, className, ...props }, ref) => {
392
+ const Comp = asChild ? Slot : 'button';
393
+
394
+ const button = (
395
+ <Comp
396
+ ref={ref}
397
+ data-sidebar="menu-button"
398
+ data-active={isActive}
399
+ className={classNames(
400
+ 'display-flex w-100 flex-auto p-sm br-sm g-lg flex-direction-row flex-auto align-items-center font-size-sm bw-0 font-weight-medium text-align-left td-none hover:background-color-tertiary font-color-base cursor-pointer',
401
+ {
402
+ 'background-color-tertiary': isActive,
403
+ 'background-color-transparent': !isActive,
404
+ },
405
+ className
406
+ )}
407
+ {...props}
408
+ >
409
+ {props.children}
410
+ </Comp>
411
+ );
412
+
413
+ return button;
414
+ });
415
+ SidebarMenuButton.displayName = 'SidebarMenuButton';
416
+
417
+ const SidebarGroup = React.forwardRef<
418
+ HTMLDivElement,
419
+ React.ComponentProps<'div'>
420
+ >(({ className, ...props }, ref) => {
421
+ return (
422
+ <div
423
+ ref={ref}
424
+ data-sidebar="group"
425
+ className={classNames(
426
+ 'position-relative p-h-md p-right-0-desktop display-flex w-100 minw-0 flex-direction-column',
427
+ className
428
+ )}
429
+ {...props}
430
+ />
431
+ );
432
+ });
433
+ SidebarGroup.displayName = 'SidebarGroup';
434
+
435
+ const SidebarGroupLabel = React.forwardRef<
436
+ HTMLDivElement,
437
+ React.ComponentProps<'div'>
438
+ >(({ className, ...props }, ref) => {
439
+ return (
440
+ <div
441
+ ref={ref}
442
+ data-sidebar="group-label"
443
+ className={classNames(
444
+ 'display-flex h-3xl align-items-center br-sm p-h-sm font-color-secondary font-size-xs font-weight-medium outline-none',
445
+ className
446
+ )}
447
+ {...props}
448
+ />
449
+ );
450
+ });
451
+ SidebarGroupLabel.displayName = 'SidebarGroupLabel';
452
+
453
+ const SidebarMenuSub = React.forwardRef<
454
+ HTMLUListElement,
455
+ React.ComponentProps<'ul'>
456
+ >(({ className, ...props }, ref) => (
457
+ <ul
458
+ ref={ref}
459
+ data-sidebar="menu-sub"
460
+ className={classNames(
461
+ 'display-flex min-w-0 m-left-xl p-left-sm flex-direction-column g-2xs bw-left-sm border-color-default',
462
+ className
463
+ )}
464
+ style={{
465
+ listStyle: 'none',
466
+ }}
467
+ {...props}
468
+ />
469
+ ));
470
+ SidebarMenuSub.displayName = 'SidebarMenuSub';
471
+
472
+ const SidebarMenuSubItem = React.forwardRef<
473
+ HTMLLIElement,
474
+ React.ComponentProps<'li'>
475
+ >(({ ...props }, ref) => <li ref={ref} {...props} />);
476
+ SidebarMenuSubItem.displayName = 'SidebarMenuSubItem';
477
+
478
+ const SidebarMenuSubButton = React.forwardRef<
479
+ HTMLAnchorElement,
480
+ React.ComponentProps<'a'> & {
481
+ asChild?: boolean;
482
+ isActive?: boolean;
483
+ }
484
+ >(({ asChild = false, isActive, className, ...props }, ref) => {
485
+ const Comp = asChild ? Slot : 'a';
486
+
487
+ return (
488
+ <Comp
489
+ ref={ref}
490
+ data-sidebar="menu-sub-button"
491
+ data-active={isActive}
492
+ className={classNames(
493
+ 'display-flex td-none h-4xl p-left-lg font-color-base minw-0 align-items-center gap-sm overflow-hidden br-sm outline-none hover:background-color-tertiary',
494
+ {
495
+ 'background-color-tertiary': isActive,
496
+ 'background-color-transparent': !isActive,
497
+ },
498
+ className
499
+ )}
500
+ {...props}
501
+ />
502
+ );
503
+ });
504
+ SidebarMenuSubButton.displayName = 'SidebarMenuSubButton';
505
+
506
+ const SidebarMenuAction = React.forwardRef<
507
+ HTMLButtonElement,
508
+ React.ComponentProps<'button'> & {
509
+ asChild?: boolean;
510
+ }
511
+ >(({ className, asChild = false, ...props }, ref) => {
512
+ const Comp = asChild ? Slot : 'button';
513
+
514
+ return (
515
+ <Comp
516
+ ref={ref}
517
+ data-sidebar="menu-action"
518
+ className={classNames(
519
+ 'position-absolute lh-none p-xs font-color-secondary cursor-pointer hover:font-color-base minw-0 align-items-center bw-0 br-sm outline-none background-color-transparent hover:background-color-tertiary',
520
+ className
521
+ )}
522
+ style={{
523
+ top: 'var(--size-spacing-xs)',
524
+ right: 'var(--size-spacing-xs)',
525
+ }}
526
+ {...props}
527
+ />
528
+ );
529
+ });
530
+ SidebarMenuAction.displayName = 'SidebarMenuAction';
531
+
532
+ const SidebarRail = React.forwardRef<
533
+ HTMLButtonElement,
534
+ React.ComponentProps<'button'>
535
+ >(({ className, ...props }, ref) => {
536
+ const { open, toggleSidebar } = useSidebar();
537
+
538
+ const caretIcon = open ? 'caret-sm-left' : 'caret-sm-right';
539
+
540
+ return (
541
+ <button
542
+ ref={ref}
543
+ data-sidebar="rail"
544
+ aria-label="Toggle Sidebar"
545
+ tabIndex={-1}
546
+ onClick={toggleSidebar}
547
+ title="Toggle Sidebar"
548
+ className={classNames(
549
+ 'hover-show-child display-flex p-top-5xl justify-content-center position-absolute background-color-transparent bw-0',
550
+ {
551
+ 'cursor-w-resize': open,
552
+ 'cursor-e-resize': !open,
553
+ },
554
+ className
555
+ )}
556
+ style={{
557
+ top: '0',
558
+ bottom: '0',
559
+ right: '-1rem',
560
+ width: '1rem',
561
+ zIndex: '-1',
562
+ }}
563
+ {...props}
564
+ >
565
+ <Box
566
+ radius="xl"
567
+ background="primary"
568
+ color="secondary"
569
+ borderWidth="sm"
570
+ padding="xs"
571
+ margin="0 0 0 sm"
572
+ shadow="xs"
573
+ className={classNames(
574
+ 'hover-child',
575
+ {
576
+ 'cursor-w-resize': open,
577
+ 'cursor-e-resize': !open,
578
+ },
579
+ className
580
+ )}
581
+ >
582
+ <Icon name={caretIcon} />
583
+ </Box>
584
+ </button>
585
+ );
586
+ });
587
+ SidebarRail.displayName = 'SidebarRail';
588
+
589
+ const SidebarMenuBadge = React.forwardRef<
590
+ HTMLDivElement,
591
+ React.ComponentProps<'div'>
592
+ >(({ className, ...props }, ref) => (
593
+ <div
594
+ ref={ref}
595
+ data-sidebar="menu-badge"
596
+ className={classNames(
597
+ 'position-absolute font-size-xs cursor-default lh-none p-xs font-color-base minw-0 align-items-center bw-0 br-sm outline-none background-color-transparent',
598
+ className
599
+ )}
600
+ style={{
601
+ top: 'var(--size-spacing-sm)',
602
+ right: 'var(--size-spacing-xs)',
603
+ }}
604
+ {...props}
605
+ />
606
+ ));
607
+ SidebarMenuBadge.displayName = 'SidebarMenuBadge';
608
+
609
+ export {
610
+ Sidebar,
611
+ SidebarContent,
612
+ SidebarFooter,
613
+ SidebarGroup,
614
+ SidebarGroupLabel,
615
+ SidebarHeader,
616
+ SidebarInset,
617
+ SidebarMenu,
618
+ SidebarMenuAction,
619
+ SidebarMenuBadge,
620
+ SidebarMenuButton,
621
+ SidebarMenuItem,
622
+ SidebarMenuSub,
623
+ SidebarMenuSubButton,
624
+ SidebarMenuSubItem,
625
+ SidebarProvider,
626
+ SidebarRail,
627
+ SidebarTrigger,
628
+ useSidebar,
629
+ };
@@ -1,6 +1,6 @@
1
1
  export * from './useBreakpoint/useBreakpoint';
2
2
  export * from './useIsMobile/useIsMobile';
3
- export * from './useIsomorphicLayoutEffect/useIsomorphicLayouEffect';
3
+ export * from './useIsomorphicLayoutEffect/useIsomorphicLayoutEffect';
4
4
  export * from './useOpenClose/useOpenClose';
5
5
  export * from './useTheme/useTheme';
6
6
  export * from './useWindowSize/useWindowSize';
@@ -2,7 +2,7 @@ import { useState } from 'react';
2
2
  import { BREAKPOINTS } from '../../lib/tokens';
3
3
  import { Breakpoint } from '../../types';
4
4
  import { useWindowSize } from '../useWindowSize/useWindowSize';
5
- import { useIsomorphicLayoutEffect } from '../useIsomorphicLayoutEffect/useIsomorphicLayouEffect';
5
+ import { useIsomorphicLayoutEffect } from '../useIsomorphicLayoutEffect/useIsomorphicLayoutEffect';
6
6
 
7
7
  const defaultBreakpoint: Breakpoint = { name: 'base', minWidth: 0 };
8
8
 
package/src/index.ts CHANGED
@@ -11,6 +11,7 @@ export * from './components/DatePicker/DatePicker';
11
11
  export * from './components/Details/Details';
12
12
  export * from './components/Details/DetailsSummary';
13
13
  export * from './components/Drawer/Drawer';
14
+ export * from './components/DropdownMenu/DropdownMenu';
14
15
  export * from './components/FormControl/FormControl';
15
16
  export * from './components/Formik/FormikCheckboxInput/FormikCheckboxInput';
16
17
  export * from './components/Formik/FormikRadioGroup/FormikRadioGroup';
@@ -36,6 +37,7 @@ export * from './components/ResponsiveProvider/ResponsiveProvider';
36
37
  export * from './components/SelectInput/SelectInput';
37
38
  export * from './components/SelectInputInset/SelectInputInset';
38
39
  export * from './components/SelectInputNative/SelectInputNative';
40
+ export * from './components/Sidebar/Sidebar';
39
41
  export * from './components/Spinner/Spinner';
40
42
  export * from './components/Table/Table';
41
43
  export * from './components/TextareaInput/TextareaInput';
@@ -56,7 +56,6 @@
56
56
  }
57
57
  }
58
58
 
59
-
60
59
  @keyframes slideInUp {
61
60
  from {
62
61
  transform: translateY(100%);
@@ -8,6 +8,10 @@
8
8
  box-sizing: border-box;
9
9
  }
10
10
 
11
+ * {
12
+ border-color: var(--color-border-subtle);
13
+ }
14
+
11
15
  html,
12
16
  body {
13
17
  height: 100%;
@@ -79,6 +83,12 @@ a:not([class]) {
79
83
  text-decoration-skip-ink: auto;
80
84
  }
81
85
 
86
+ a,
87
+ a:visited {
88
+ color: inherit;
89
+ text-decoration: none;
90
+ }
91
+
82
92
  // Make images easier to work with
83
93
  img {
84
94
  max-width: 100%;