terminoir 0.4.0 → 0.5.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.
Files changed (98) hide show
  1. package/README.md +38 -28
  2. package/css/components.css +62 -0
  3. package/css/shell.css +32 -1
  4. package/dist/components/AccentSwatches.svelte +1 -1
  5. package/dist/components/AppShell.svelte +33 -5
  6. package/dist/components/AppShell.svelte.d.ts +10 -1
  7. package/dist/components/AppShellNavLink.svelte +2 -4
  8. package/dist/components/Button.svelte +32 -14
  9. package/dist/components/Button.svelte.d.ts +2 -0
  10. package/dist/components/CalendarGrid.svelte +245 -0
  11. package/dist/components/CalendarGrid.svelte.d.ts +12 -0
  12. package/dist/components/CanvasStage.svelte +81 -0
  13. package/dist/components/CanvasStage.svelte.d.ts +19 -0
  14. package/dist/components/Centered.svelte +36 -0
  15. package/dist/components/Centered.svelte.d.ts +10 -0
  16. package/dist/components/Checkbox.svelte +64 -12
  17. package/dist/components/Checkbox.svelte.d.ts +5 -0
  18. package/dist/components/Chip.svelte +90 -2
  19. package/dist/components/Chip.svelte.d.ts +8 -0
  20. package/dist/components/CommandPalette.svelte +178 -97
  21. package/dist/components/CommandPalette.svelte.d.ts +15 -8
  22. package/dist/components/ContextMenu.svelte +68 -0
  23. package/dist/components/ContextMenu.svelte.d.ts +12 -0
  24. package/dist/components/DataTable.svelte +139 -0
  25. package/dist/components/DataTable.svelte.d.ts +39 -0
  26. package/dist/components/DescriptionList.svelte +41 -0
  27. package/dist/components/DescriptionList.svelte.d.ts +8 -0
  28. package/dist/components/Dialog.svelte +2 -1
  29. package/dist/components/Disclosure.svelte +55 -0
  30. package/dist/components/Disclosure.svelte.d.ts +10 -0
  31. package/dist/components/DocumentViewer.svelte +105 -0
  32. package/dist/components/DocumentViewer.svelte.d.ts +23 -0
  33. package/dist/components/Drawer.svelte +119 -0
  34. package/dist/components/Drawer.svelte.d.ts +16 -0
  35. package/dist/components/DropdownMenu.svelte +46 -62
  36. package/dist/components/DropdownMenu.svelte.d.ts +7 -6
  37. package/dist/components/FieldMessage.svelte +9 -0
  38. package/dist/components/FieldMessage.svelte.d.ts +8 -0
  39. package/dist/components/FileButton.svelte +44 -0
  40. package/dist/components/FileButton.svelte.d.ts +16 -0
  41. package/dist/components/FormRow.svelte +62 -0
  42. package/dist/components/FormRow.svelte.d.ts +13 -0
  43. package/dist/components/IconField.svelte +80 -5
  44. package/dist/components/IconField.svelte.d.ts +2 -0
  45. package/dist/components/Inline.svelte +52 -0
  46. package/dist/components/Inline.svelte.d.ts +16 -0
  47. package/dist/components/Input.svelte +23 -3
  48. package/dist/components/Input.svelte.d.ts +9 -0
  49. package/dist/components/List.svelte +42 -0
  50. package/dist/components/List.svelte.d.ts +13 -0
  51. package/dist/components/ListItem.svelte +114 -0
  52. package/dist/components/ListItem.svelte.d.ts +17 -0
  53. package/dist/components/MenuItems.svelte +44 -0
  54. package/dist/components/MenuItems.svelte.d.ts +8 -0
  55. package/dist/components/NavSection.svelte +45 -0
  56. package/dist/components/NavSection.svelte.d.ts +12 -0
  57. package/dist/components/Page.svelte +53 -0
  58. package/dist/components/Page.svelte.d.ts +12 -0
  59. package/dist/components/PageHeader.svelte +91 -0
  60. package/dist/components/PageHeader.svelte.d.ts +15 -0
  61. package/dist/components/Prose.svelte +33 -0
  62. package/dist/components/Prose.svelte.d.ts +8 -0
  63. package/dist/components/RadioCards.svelte +68 -0
  64. package/dist/components/RadioCards.svelte.d.ts +11 -0
  65. package/dist/components/SearchPicker.svelte +171 -0
  66. package/dist/components/SearchPicker.svelte.d.ts +19 -0
  67. package/dist/components/Section.svelte +71 -0
  68. package/dist/components/Section.svelte.d.ts +12 -0
  69. package/dist/components/SegmentedControl.svelte +63 -0
  70. package/dist/components/SegmentedControl.svelte.d.ts +12 -0
  71. package/dist/components/Select.svelte +22 -4
  72. package/dist/components/Select.svelte.d.ts +9 -1
  73. package/dist/components/Spacer.svelte +6 -0
  74. package/dist/components/Spacer.svelte.d.ts +26 -0
  75. package/dist/components/Stack.svelte +37 -0
  76. package/dist/components/Stack.svelte.d.ts +12 -0
  77. package/dist/components/Switch.svelte +6 -2
  78. package/dist/components/Switch.svelte.d.ts +2 -0
  79. package/dist/components/Tabs.svelte +6 -4
  80. package/dist/components/Text.svelte +68 -0
  81. package/dist/components/Text.svelte.d.ts +15 -0
  82. package/dist/components/Textarea.svelte +19 -3
  83. package/dist/components/Textarea.svelte.d.ts +6 -0
  84. package/dist/components/TitleInput.svelte +62 -0
  85. package/dist/components/TitleInput.svelte.d.ts +10 -0
  86. package/dist/index.d.ts +35 -2
  87. package/dist/index.js +31 -0
  88. package/dist/utils/calendar.d.ts +35 -0
  89. package/dist/utils/calendar.js +61 -0
  90. package/dist/utils/commands.d.ts +22 -0
  91. package/dist/utils/commands.js +17 -0
  92. package/dist/utils/componentTypes.d.ts +41 -0
  93. package/dist/utils/componentTypes.js +1 -0
  94. package/dist/utils/listNav.d.ts +2 -0
  95. package/dist/utils/listNav.js +8 -0
  96. package/dist/utils/menu.d.ts +10 -0
  97. package/dist/utils/menu.js +1 -0
  98. package/package.json +5 -3
@@ -7,19 +7,43 @@
7
7
  label = 'Icon',
8
8
  value = $bindable('inbox' as IconPickerId | string),
9
9
  options = ICON_PICKER_OPTIONS,
10
+ compact = false,
10
11
  class: className = '',
11
12
  }: {
12
13
  label?: string;
13
14
  value?: string;
14
15
  options?: ReadonlyArray<{ id: string; label: string }>;
16
+ /** A button showing the current icon that opens the grid in a popover (for forms). */
17
+ compact?: boolean;
15
18
  class?: string;
16
19
  } = $props();
17
20
 
18
- const groupId = `terminoir-icon-field-${Math.random().toString(36).slice(2, 8)}`;
21
+ const groupId = $props.id();
22
+ let open = $state(false);
23
+ let root: HTMLDivElement | undefined = $state();
24
+ const current = $derived(options.find((o) => o.id === value));
25
+
26
+ function pick(id: string) {
27
+ value = id;
28
+ open = false;
29
+ }
30
+
31
+ $effect(() => {
32
+ if (!open) return;
33
+ const onPointer = (event: PointerEvent) => {
34
+ if (root && !root.contains(event.target as Node)) open = false;
35
+ };
36
+ const onKey = (event: KeyboardEvent) => event.key === 'Escape' && (open = false);
37
+ document.addEventListener('pointerdown', onPointer);
38
+ document.addEventListener('keydown', onKey);
39
+ return () => {
40
+ document.removeEventListener('pointerdown', onPointer);
41
+ document.removeEventListener('keydown', onKey);
42
+ };
43
+ });
19
44
  </script>
20
45
 
21
- <fieldset class={cn('terminoir-icon-field glass-palette', className)} aria-labelledby={`${groupId}-label`}>
22
- <legend id={`${groupId}-label`} class="terminoir-field__label">{label}</legend>
46
+ {#snippet grid()}
23
47
  <div class="terminoir-icon-field__grid" role="radiogroup" aria-label={label}>
24
48
  {#each options as option (option.id)}
25
49
  <button
@@ -30,10 +54,61 @@
30
54
  aria-checked={value === option.id}
31
55
  aria-label={option.label}
32
56
  title={option.label}
33
- onclick={() => (value = option.id)}
57
+ onclick={() => pick(option.id)}
34
58
  >
35
59
  <NavIcon name={option.id} />
36
60
  </button>
37
61
  {/each}
38
62
  </div>
39
- </fieldset>
63
+ {/snippet}
64
+
65
+ {#if compact}
66
+ <div bind:this={root} class={cn('terminoir-field terminoir-icon-field--compact', className)}>
67
+ <span id={`${groupId}-label`} class="terminoir-field__label">{label}</span>
68
+ <button
69
+ type="button"
70
+ class="terminoir-icon-field__trigger terminoir-input terminoir-focusable"
71
+ aria-labelledby={`${groupId}-label`}
72
+ aria-haspopup="dialog"
73
+ aria-expanded={open}
74
+ onclick={() => (open = !open)}
75
+ >
76
+ {#if value}<NavIcon name={value} />{/if}
77
+ <span>{current?.label ?? (value ? value : 'Choose an icon')}</span>
78
+ </button>
79
+ {#if open}
80
+ <div class="terminoir-icon-field__popover glass-popover" role="dialog" aria-label={label}>
81
+ {@render grid()}
82
+ </div>
83
+ {/if}
84
+ </div>
85
+ {:else}
86
+ <fieldset class={cn('terminoir-icon-field glass-palette', className)} aria-labelledby={`${groupId}-label`}>
87
+ <legend id={`${groupId}-label`} class="terminoir-field__label">{label}</legend>
88
+ {@render grid()}
89
+ </fieldset>
90
+ {/if}
91
+
92
+ <style>
93
+ .terminoir-icon-field--compact { position: relative; }
94
+ .terminoir-icon-field__trigger {
95
+ display: flex;
96
+ align-items: center;
97
+ gap: var(--space-2);
98
+ text-align: left;
99
+ cursor: pointer;
100
+ }
101
+ .terminoir-icon-field__popover {
102
+ position: absolute;
103
+ top: calc(100% + var(--space-1));
104
+ left: 0;
105
+ z-index: 40;
106
+ padding: var(--space-2);
107
+ width: max-content;
108
+ max-width: 18rem;
109
+ }
110
+ .terminoir-icon-field__popover .terminoir-icon-field__grid {
111
+ grid-template-columns: repeat(6, 2.5rem);
112
+ margin-top: 0;
113
+ }
114
+ </style>
@@ -5,6 +5,8 @@ type $$ComponentProps = {
5
5
  id: string;
6
6
  label: string;
7
7
  }>;
8
+ /** A button showing the current icon that opens the grid in a popover (for forms). */
9
+ compact?: boolean;
8
10
  class?: string;
9
11
  };
10
12
  declare const IconField: import("svelte").Component<$$ComponentProps, {}, "value">;
@@ -0,0 +1,52 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from 'svelte';
3
+ import { cn } from '../utils/cn.js';
4
+
5
+ type Gap = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 8;
6
+
7
+ let {
8
+ gap = 2,
9
+ align = 'center',
10
+ justify = 'start',
11
+ wrap = true,
12
+ fill = false,
13
+ class: className = '',
14
+ children,
15
+ ...rest
16
+ }: {
17
+ /** Spacing between children, as a `--space-*` step. */
18
+ gap?: Gap;
19
+ align?: 'start' | 'center' | 'end' | 'baseline' | 'stretch';
20
+ justify?: 'start' | 'center' | 'end' | 'between';
21
+ wrap?: boolean;
22
+ /** Children share the row equally (e.g. two inputs side by side). */
23
+ fill?: boolean;
24
+ class?: string;
25
+ children?: Snippet;
26
+ } & Record<string, unknown> = $props();
27
+
28
+ const JUSTIFY = { start: 'flex-start', center: 'center', end: 'flex-end', between: 'space-between' };
29
+ const ALIGN = { start: 'flex-start', center: 'center', end: 'flex-end', baseline: 'baseline', stretch: 'stretch' };
30
+ </script>
31
+
32
+ <div
33
+ class={cn('terminoir-inline', fill && 'terminoir-inline--fill', className)}
34
+ style:gap={`var(--space-${gap})`}
35
+ style:align-items={ALIGN[align]}
36
+ style:justify-content={JUSTIFY[justify]}
37
+ style:flex-wrap={wrap ? 'wrap' : 'nowrap'}
38
+ {...rest}
39
+ >
40
+ {@render children?.()}
41
+ </div>
42
+
43
+ <style>
44
+ .terminoir-inline {
45
+ display: flex;
46
+ min-width: 0;
47
+ }
48
+ .terminoir-inline--fill > :global(*) {
49
+ flex: 1 1 0;
50
+ min-width: 0;
51
+ }
52
+ </style>
@@ -0,0 +1,16 @@
1
+ import type { Snippet } from 'svelte';
2
+ type Gap = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 8;
3
+ type $$ComponentProps = {
4
+ /** Spacing between children, as a `--space-*` step. */
5
+ gap?: Gap;
6
+ align?: 'start' | 'center' | 'end' | 'baseline' | 'stretch';
7
+ justify?: 'start' | 'center' | 'end' | 'between';
8
+ wrap?: boolean;
9
+ /** Children share the row equally (e.g. two inputs side by side). */
10
+ fill?: boolean;
11
+ class?: string;
12
+ children?: Snippet;
13
+ } & Record<string, unknown>;
14
+ declare const Inline: import("svelte").Component<$$ComponentProps, {}, "">;
15
+ type Inline = ReturnType<typeof Inline>;
16
+ export default Inline;
@@ -1,8 +1,14 @@
1
1
  <script lang="ts">
2
2
  import { cn } from '../utils/cn.js';
3
+ import FieldMessage from './FieldMessage.svelte';
3
4
 
4
5
  let {
5
6
  label,
7
+ hideLabel = false,
8
+ quiet = false,
9
+ width,
10
+ hint,
11
+ error,
6
12
  id,
7
13
  value = $bindable(''),
8
14
  placeholder = '',
@@ -11,6 +17,15 @@
11
17
  ...rest
12
18
  }: {
13
19
  label: string;
20
+ /** Keep the label for screen readers only (compact rows, toolbars). */
21
+ hideLabel?: boolean;
22
+ /** Borderless until hovered or focused, for inline editing (property rows). */
23
+ quiet?: boolean;
24
+ /** Fixed width for compact fields (e.g. `4rem` for a page number); fills the row otherwise. */
25
+ width?: string;
26
+ hint?: string;
27
+ /** Error text; also marks the input invalid. */
28
+ error?: string;
14
29
  id?: string;
15
30
  value?: string;
16
31
  placeholder?: string;
@@ -18,17 +33,22 @@
18
33
  class?: string;
19
34
  } & Record<string, unknown> = $props();
20
35
 
21
- const inputId = $derived(id ?? `terminoir-input-${Math.random().toString(36).slice(2, 8)}`);
36
+ const fallbackId = $props.id();
37
+ const inputId = $derived(id ?? fallbackId);
38
+ const messageId = $derived(`${inputId}-message`);
22
39
  </script>
23
40
 
24
- <div class={cn('terminoir-field', className)}>
25
- <label class="terminoir-field__label" for={inputId}>{label}</label>
41
+ <div class={cn('terminoir-field', quiet && 'terminoir-field--quiet', className)} style:width>
42
+ <label class={cn('terminoir-field__label', hideLabel && 'terminoir-sr-only')} for={inputId}>{label}</label>
26
43
  <input
27
44
  id={inputId}
28
45
  class="terminoir-input terminoir-focusable"
29
46
  bind:value
30
47
  {placeholder}
31
48
  {disabled}
49
+ aria-invalid={error ? true : undefined}
50
+ aria-describedby={error || hint ? messageId : undefined}
32
51
  {...rest}
33
52
  />
53
+ <FieldMessage id={messageId} {hint} {error} />
34
54
  </div>
@@ -1,5 +1,14 @@
1
1
  type $$ComponentProps = {
2
2
  label: string;
3
+ /** Keep the label for screen readers only (compact rows, toolbars). */
4
+ hideLabel?: boolean;
5
+ /** Borderless until hovered or focused, for inline editing (property rows). */
6
+ quiet?: boolean;
7
+ /** Fixed width for compact fields (e.g. `4rem` for a page number); fills the row otherwise. */
8
+ width?: string;
9
+ hint?: string;
10
+ /** Error text; also marks the input invalid. */
11
+ error?: string;
3
12
  id?: string;
4
13
  value?: string;
5
14
  placeholder?: string;
@@ -0,0 +1,42 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from 'svelte';
3
+ import { cn } from '../utils/cn.js';
4
+
5
+ let {
6
+ label,
7
+ bordered = true,
8
+ class: className = '',
9
+ children,
10
+ }: {
11
+ /** Accessible list name. */
12
+ label?: string;
13
+ /** Card-style border around the rows; off for lists inside other surfaces. */
14
+ bordered?: boolean;
15
+ class?: string;
16
+ /** `ListItem`s. */
17
+ children?: Snippet;
18
+ } = $props();
19
+ </script>
20
+
21
+ <ul class={cn('terminoir-list', bordered && 'terminoir-list--bordered', className)} aria-label={label}>
22
+ {@render children?.()}
23
+ </ul>
24
+
25
+ <style>
26
+ .terminoir-list {
27
+ list-style: none;
28
+ margin: 0;
29
+ padding: 0;
30
+ display: flex;
31
+ flex-direction: column;
32
+ min-width: 0;
33
+ }
34
+ .terminoir-list--bordered {
35
+ border: 1px solid var(--border);
36
+ border-radius: var(--radius);
37
+ overflow: hidden;
38
+ }
39
+ .terminoir-list--bordered > :global(li + li) {
40
+ border-top: 1px solid color-mix(in srgb, var(--border) 60%, transparent);
41
+ }
42
+ </style>
@@ -0,0 +1,13 @@
1
+ import type { Snippet } from 'svelte';
2
+ type $$ComponentProps = {
3
+ /** Accessible list name. */
4
+ label?: string;
5
+ /** Card-style border around the rows; off for lists inside other surfaces. */
6
+ bordered?: boolean;
7
+ class?: string;
8
+ /** `ListItem`s. */
9
+ children?: Snippet;
10
+ };
11
+ declare const List: import("svelte").Component<$$ComponentProps, {}, "">;
12
+ type List = ReturnType<typeof List>;
13
+ export default List;
@@ -0,0 +1,114 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from 'svelte';
3
+ import { cn } from '../utils/cn.js';
4
+
5
+ let {
6
+ title,
7
+ description,
8
+ active = false,
9
+ onclick,
10
+ class: className = '',
11
+ leading,
12
+ trailing,
13
+ children,
14
+ ...rest
15
+ }: {
16
+ title?: string;
17
+ description?: string;
18
+ /** Highlighted row (keyboard focus in a listbox, current selection). */
19
+ active?: boolean;
20
+ /** Makes the whole row a button. Keep `trailing` free of other buttons when set. */
21
+ onclick?: (event: MouseEvent) => void;
22
+ class?: string;
23
+ leading?: Snippet;
24
+ trailing?: Snippet;
25
+ /** Replaces title/description for custom content. */
26
+ children?: Snippet;
27
+ } & Record<string, unknown> = $props();
28
+ </script>
29
+
30
+ {#snippet body()}
31
+ {#if leading}<span class="terminoir-list-item__leading">{@render leading()}</span>{/if}
32
+ <span class="terminoir-list-item__text">
33
+ {#if children}
34
+ {@render children()}
35
+ {:else}
36
+ {#if title}<span class="terminoir-list-item__title">{title}</span>{/if}
37
+ {#if description}<span class="terminoir-list-item__description">{description}</span>{/if}
38
+ {/if}
39
+ </span>
40
+ {#if trailing}<span class="terminoir-list-item__trailing">{@render trailing()}</span>{/if}
41
+ {/snippet}
42
+
43
+ <li class={cn('terminoir-list-item', active && 'terminoir-list-item--active', className)} {...rest}>
44
+ {#if onclick}
45
+ <button type="button" class="terminoir-list-item__row terminoir-list-item__button terminoir-focusable" {onclick}>
46
+ {@render body()}
47
+ </button>
48
+ {:else}
49
+ <div class="terminoir-list-item__row">{@render body()}</div>
50
+ {/if}
51
+ </li>
52
+
53
+ <style>
54
+ .terminoir-list-item { min-width: 0; }
55
+ .terminoir-list-item__row {
56
+ display: flex;
57
+ align-items: center;
58
+ gap: var(--space-3);
59
+ width: 100%;
60
+ min-height: 2.75rem;
61
+ padding: 0.5rem 0.85rem;
62
+ box-sizing: border-box;
63
+ }
64
+ .terminoir-list-item__button {
65
+ border: 0;
66
+ background: transparent;
67
+ color: inherit;
68
+ font: inherit;
69
+ text-align: left;
70
+ cursor: pointer;
71
+ border-radius: 0;
72
+ }
73
+ .terminoir-list-item__button:hover,
74
+ .terminoir-list-item--active > .terminoir-list-item__row {
75
+ background: color-mix(in srgb, var(--foreground) 6%, transparent);
76
+ }
77
+ .terminoir-list-item__leading {
78
+ display: inline-flex;
79
+ align-items: center;
80
+ justify-content: center;
81
+ flex-shrink: 0;
82
+ min-width: 1.25rem;
83
+ color: var(--muted-foreground);
84
+ }
85
+ .terminoir-list-item__text {
86
+ flex: 1;
87
+ min-width: 0;
88
+ display: flex;
89
+ flex-direction: column;
90
+ gap: 0.1rem;
91
+ }
92
+ .terminoir-list-item__title {
93
+ font-size: 0.875rem;
94
+ color: var(--foreground);
95
+ overflow: hidden;
96
+ text-overflow: ellipsis;
97
+ white-space: nowrap;
98
+ }
99
+ .terminoir-list-item__description {
100
+ font-size: 0.75rem;
101
+ color: var(--muted-foreground);
102
+ overflow: hidden;
103
+ text-overflow: ellipsis;
104
+ white-space: nowrap;
105
+ }
106
+ .terminoir-list-item__trailing {
107
+ display: inline-flex;
108
+ align-items: center;
109
+ gap: var(--space-2);
110
+ flex-shrink: 0;
111
+ color: var(--muted-foreground);
112
+ font-size: 0.8125rem;
113
+ }
114
+ </style>
@@ -0,0 +1,17 @@
1
+ import type { Snippet } from 'svelte';
2
+ type $$ComponentProps = {
3
+ title?: string;
4
+ description?: string;
5
+ /** Highlighted row (keyboard focus in a listbox, current selection). */
6
+ active?: boolean;
7
+ /** Makes the whole row a button. Keep `trailing` free of other buttons when set. */
8
+ onclick?: (event: MouseEvent) => void;
9
+ class?: string;
10
+ leading?: Snippet;
11
+ trailing?: Snippet;
12
+ /** Replaces title/description for custom content. */
13
+ children?: Snippet;
14
+ } & Record<string, unknown>;
15
+ declare const ListItem: import("svelte").Component<$$ComponentProps, {}, "">;
16
+ type ListItem = ReturnType<typeof ListItem>;
17
+ export default ListItem;
@@ -0,0 +1,44 @@
1
+ <script lang="ts">
2
+ import type { MenuItem } from '../utils/menu.js';
3
+ import NavIcon from './NavIcon.svelte';
4
+
5
+ let { items, onselect }: { items: MenuItem[]; onselect: (item: MenuItem) => void } = $props();
6
+ </script>
7
+
8
+ {#each items as item (item.id)}
9
+ <li role="none">
10
+ <button
11
+ type="button"
12
+ role="menuitem"
13
+ class="terminoir-menu__item terminoir-focusable"
14
+ class:terminoir-menu__item--danger={item.tone === 'danger'}
15
+ disabled={item.disabled}
16
+ onclick={() => onselect(item)}
17
+ >
18
+ {#if item.icon}<NavIcon name={item.icon} />{/if}
19
+ <span>{item.label}</span>
20
+ </button>
21
+ </li>
22
+ {/each}
23
+
24
+ <style>
25
+ .terminoir-menu__item {
26
+ display: flex;
27
+ align-items: center;
28
+ gap: var(--space-2);
29
+ width: 100%;
30
+ text-align: left;
31
+ border: 0;
32
+ background: transparent;
33
+ color: var(--foreground);
34
+ border-radius: calc(var(--radius) - 4px);
35
+ padding: 0.45rem 0.6rem;
36
+ font: inherit;
37
+ font-size: 0.875rem;
38
+ cursor: pointer;
39
+ white-space: nowrap;
40
+ }
41
+ .terminoir-menu__item:hover:not(:disabled) { background: color-mix(in srgb, var(--foreground) 8%, transparent); }
42
+ .terminoir-menu__item:disabled { opacity: 0.5; cursor: not-allowed; }
43
+ .terminoir-menu__item--danger { color: var(--destructive); }
44
+ </style>
@@ -0,0 +1,8 @@
1
+ import type { MenuItem } from '../utils/menu.js';
2
+ type $$ComponentProps = {
3
+ items: MenuItem[];
4
+ onselect: (item: MenuItem) => void;
5
+ };
6
+ declare const MenuItems: import("svelte").Component<$$ComponentProps, {}, "">;
7
+ type MenuItems = ReturnType<typeof MenuItems>;
8
+ export default MenuItems;
@@ -0,0 +1,45 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from 'svelte';
3
+ import { cn } from '../utils/cn.js';
4
+
5
+ let {
6
+ title,
7
+ class: className = '',
8
+ action,
9
+ children,
10
+ }: {
11
+ title: string;
12
+ class?: string;
13
+ /** Small trailing control, e.g. an IconButton to add an entry. */
14
+ action?: Snippet;
15
+ /** AppShellNavLink rows. */
16
+ children?: Snippet;
17
+ } = $props();
18
+ </script>
19
+
20
+ <nav class={cn('terminoir-nav-section', className)} aria-label={title}>
21
+ <div class="terminoir-nav-section__head">
22
+ <span class="terminoir-nav-section__title">{title}</span>
23
+ {#if action}{@render action()}{/if}
24
+ </div>
25
+ <div class="terminoir-nav-section__items">{@render children?.()}</div>
26
+ </nav>
27
+
28
+ <style>
29
+ .terminoir-nav-section { display: flex; flex-direction: column; gap: var(--space-1); }
30
+ .terminoir-nav-section__head {
31
+ display: flex;
32
+ align-items: center;
33
+ justify-content: space-between;
34
+ min-height: 1.75rem;
35
+ padding-left: 0.65rem;
36
+ }
37
+ .terminoir-nav-section__title {
38
+ font-size: 0.6875rem;
39
+ font-weight: 600;
40
+ letter-spacing: 0.06em;
41
+ text-transform: uppercase;
42
+ color: var(--muted-foreground);
43
+ }
44
+ .terminoir-nav-section__items { display: flex; flex-direction: column; gap: 0.1rem; }
45
+ </style>
@@ -0,0 +1,12 @@
1
+ import type { Snippet } from 'svelte';
2
+ type $$ComponentProps = {
3
+ title: string;
4
+ class?: string;
5
+ /** Small trailing control, e.g. an IconButton to add an entry. */
6
+ action?: Snippet;
7
+ /** AppShellNavLink rows. */
8
+ children?: Snippet;
9
+ };
10
+ declare const NavSection: import("svelte").Component<$$ComponentProps, {}, "">;
11
+ type NavSection = ReturnType<typeof NavSection>;
12
+ export default NavSection;
@@ -0,0 +1,53 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from 'svelte';
3
+ import { cn } from '../utils/cn.js';
4
+
5
+ let {
6
+ width = 'default',
7
+ fill = false,
8
+ class: className = '',
9
+ children,
10
+ ...rest
11
+ }: {
12
+ /** Readable column width; `full` spans the stage. */
13
+ width?: 'narrow' | 'default' | 'wide' | 'full';
14
+ /** Edge-to-edge and full height with no padding, for canvases (graphs, viewers, calendars). */
15
+ fill?: boolean;
16
+ class?: string;
17
+ children?: Snippet;
18
+ } & Record<string, unknown> = $props();
19
+ </script>
20
+
21
+ <div class={cn('terminoir-page', `terminoir-page--${width}`, fill && 'terminoir-page--fill', className)} {...rest}>
22
+ {@render children?.()}
23
+ </div>
24
+
25
+ <style>
26
+ .terminoir-page {
27
+ display: flex;
28
+ flex-direction: column;
29
+ gap: var(--space-6);
30
+ width: 100%;
31
+ margin-inline: auto;
32
+ padding: var(--space-6) var(--space-6) var(--space-10);
33
+ box-sizing: border-box;
34
+ min-width: 0;
35
+ }
36
+ .terminoir-page--narrow { max-width: 40rem; }
37
+ .terminoir-page--default { max-width: 56rem; }
38
+ .terminoir-page--wide { max-width: 80rem; }
39
+ .terminoir-page--full { max-width: none; }
40
+ .terminoir-page--fill {
41
+ max-width: none;
42
+ height: 100%;
43
+ padding: 0;
44
+ gap: 0;
45
+ position: relative;
46
+ }
47
+ @media (max-width: 700px) {
48
+ .terminoir-page:not(.terminoir-page--fill) {
49
+ padding: var(--space-4) var(--space-4) var(--space-8);
50
+ gap: var(--space-5);
51
+ }
52
+ }
53
+ </style>
@@ -0,0 +1,12 @@
1
+ import type { Snippet } from 'svelte';
2
+ type $$ComponentProps = {
3
+ /** Readable column width; `full` spans the stage. */
4
+ width?: 'narrow' | 'default' | 'wide' | 'full';
5
+ /** Edge-to-edge and full height with no padding, for canvases (graphs, viewers, calendars). */
6
+ fill?: boolean;
7
+ class?: string;
8
+ children?: Snippet;
9
+ } & Record<string, unknown>;
10
+ declare const Page: import("svelte").Component<$$ComponentProps, {}, "">;
11
+ type Page = ReturnType<typeof Page>;
12
+ export default Page;