terminoir 0.18.2 → 0.19.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
@@ -124,7 +124,7 @@ Import from **`terminoir`**:
124
124
  | `Input`, `Textarea`, `Select`, `Checkbox`, `Switch` | `hideLabel` (screen-reader only), `hint`, `error`, `quiet` (borderless until hover/focus, for inline property editing); `Input` `width` for toolbar fields |
125
125
  | `Chip`, `Badge` | Status tones; `Chip` `color` dot, `onclick`, removable `onremove`, on/off toggle `pressed` (filters) |
126
126
  | `StatusChip` | On/off status: green dot + `activeLabel`, or muted `inactiveLabel` (defaults Connected / Not connected) |
127
- | `Card`, `CardGrid` | Accent tint, badge, chips, progress, meta, hover lift; `CardGrid columns={2\|3\|4}` fixes the column count on wide screens (one column on phones), also for `Panel`s |
127
+ | `Card`, `CardGrid` | Accent tint, badge, chips, progress, meta, hover lift; `CardGrid columns={2\|3\|4}` caps the column count; columns stay at least `--terminoir-card-grid-min` (20rem) wide, so a narrow grid drops to fewer. Also for `Panel`s |
128
128
  | `Panel` | Glass panel surface |
129
129
  | `Dialog` | Modal with focus trap-ish behavior + Escape |
130
130
  | `Popover`, `Tooltip` | Lightweight overlays |
@@ -132,7 +132,7 @@ Import from **`terminoir`**:
132
132
  | `Drawer` | Side sheet with title, close button and optional footer |
133
133
  | `CommandPalette` | ⌘/Ctrl+K (`hotkey`), grouped rows with icons and hints; `bind:query` + `filter={false}` + `loading` for async search; `footer` snippet |
134
134
  | `Toaster` + `toast()` | Action / undo via `action: { label, onClick }` |
135
- | `Tabs`, `SegmentedControl` | Tab panels (unique ids per instance); compact radio-group toggle |
135
+ | `Tabs`, `SegmentedControl` | Tab panels (unique ids per instance, arrow keys, optional `icon` per tab, `orientation="vertical"` for a side column that becomes a row on phones); compact radio-group toggle |
136
136
  | `AppShell` | `header` + `sidebar` snippets; mobile drawer; `fitViewport` (main scrolls, for canvases), `flush`, `headerMode="mobile"`; `toggleSidebar()` / `closeMobileDrawer()` via `bind:this` |
137
137
  | `AppShellNavLink` | `href?`, `active`, nav row with active highlight |
138
138
  | `SettingsPage`, `SettingsSection` | Settings layout — intro + stacked glass section cards |
package/css/cards.css CHANGED
@@ -9,13 +9,18 @@
9
9
  width: 100%;
10
10
  }
11
11
 
12
- .terminoir-card-grid--cols-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
13
- .terminoir-card-grid--cols-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
14
- .terminoir-card-grid--cols-4 { grid-template-columns: repeat(4, minmax(0, 1fr)); }
15
-
16
- @media (max-width: 700px) {
17
- .terminoir-card-grid[class*='terminoir-card-grid--cols-'] { grid-template-columns: minmax(0, 1fr); }
12
+ .terminoir-card-grid[class*='terminoir-card-grid--cols-'] {
13
+ /* N columns at most, each at least --terminoir-card-grid-min wide: drops to fewer columns when the grid
14
+ itself is narrow (a side column, a laptop), not only on phones. */
15
+ --terminoir-card-grid-min: 20rem;
16
+ grid-template-columns: repeat(
17
+ auto-fill,
18
+ minmax(min(100%, max(var(--terminoir-card-grid-min), calc((100% - (var(--terminoir-card-grid-cols) - 1) * var(--space-4)) / var(--terminoir-card-grid-cols)))), 1fr)
19
+ );
18
20
  }
21
+ .terminoir-card-grid--cols-2 { --terminoir-card-grid-cols: 2; }
22
+ .terminoir-card-grid--cols-3 { --terminoir-card-grid-cols: 3; }
23
+ .terminoir-card-grid--cols-4 { --terminoir-card-grid-cols: 4; }
19
24
 
20
25
  .terminoir-card {
21
26
  position: relative;
@@ -9,7 +9,8 @@
9
9
  ...rest
10
10
  }: {
11
11
  /**
12
- * Fixed column count on wide screens (one column on phones), e.g. 2 for a pair of settings panels.
12
+ * At most this many columns, e.g. 2 for a pair of settings panels. Columns stay at least
13
+ * `--terminoir-card-grid-min` (20rem) wide, so a narrow grid (a phone, beside a side column) shows fewer.
13
14
  * Omit to fit as many 280px columns as the width allows.
14
15
  */
15
16
  columns?: 2 | 3 | 4;
@@ -1,7 +1,8 @@
1
1
  import type { Snippet } from 'svelte';
2
2
  type $$ComponentProps = {
3
3
  /**
4
- * Fixed column count on wide screens (one column on phones), e.g. 2 for a pair of settings panels.
4
+ * At most this many columns, e.g. 2 for a pair of settings panels. Columns stay at least
5
+ * `--terminoir-card-grid-min` (20rem) wide, so a narrow grid (a phone, beside a side column) shows fewer.
5
6
  * Omit to fit as many 280px columns as the width allows.
6
7
  */
7
8
  columns?: 2 | 3 | 4;
@@ -1,15 +1,20 @@
1
1
  <script lang="ts">
2
2
  import type { Snippet } from 'svelte';
3
3
  import { cn } from '../utils/cn.js';
4
+ import NavIcon from './NavIcon.svelte';
4
5
 
5
6
  let {
6
7
  tabs = [],
7
8
  value = $bindable(''),
9
+ orientation = 'horizontal',
8
10
  class: className = '',
9
11
  children,
10
12
  }: {
11
- tabs: Array<{ id: string; label: string }>;
13
+ /** `icon` is a Font Awesome name, as for `NavIcon`. */
14
+ tabs: Array<{ id: string; label: string; icon?: string }>;
12
15
  value?: string;
16
+ /** `vertical`: the tabs stack in a column beside the panel (a settings sidebar); a scrolling row on phones. */
17
+ orientation?: 'horizontal' | 'vertical';
13
18
  class?: string;
14
19
  children?: Snippet;
15
20
  } = $props();
@@ -19,10 +24,27 @@
19
24
  $effect.pre(() => {
20
25
  if (!value && tabs[0]) value = tabs[0].id;
21
26
  });
27
+
28
+ const PREVIOUS = new Set(['ArrowLeft', 'ArrowUp']);
29
+ const NEXT = new Set(['ArrowRight', 'ArrowDown']);
30
+
31
+ /** Arrow keys move between tabs (either axis, so it works for both orientations), Home/End jump to the ends. */
32
+ function onkeydown(event: KeyboardEvent) {
33
+ const index = tabs.findIndex((tab) => tab.id === value);
34
+ let next = -1;
35
+ if (PREVIOUS.has(event.key)) next = (index - 1 + tabs.length) % tabs.length;
36
+ else if (NEXT.has(event.key)) next = (index + 1) % tabs.length;
37
+ else if (event.key === 'Home') next = 0;
38
+ else if (event.key === 'End') next = tabs.length - 1;
39
+ if (next < 0) return;
40
+ event.preventDefault();
41
+ value = tabs[next].id;
42
+ (event.currentTarget as HTMLElement).querySelector<HTMLElement>(`#${CSS.escape(`${uid}-tab-${value}`)}`)?.focus();
43
+ }
22
44
  </script>
23
45
 
24
- <div class={cn('terminoir-tabs', className)}>
25
- <div role="tablist" aria-orientation="horizontal" class="terminoir-tabs__list glass-chrome">
46
+ <div class={cn('terminoir-tabs', orientation === 'vertical' && 'terminoir-tabs--vertical', className)}>
47
+ <div role="tablist" aria-orientation={orientation} class="terminoir-tabs__list glass-chrome" tabindex="-1" {onkeydown}>
26
48
  {#each tabs as tab (tab.id)}
27
49
  <button
28
50
  type="button"
@@ -30,9 +52,11 @@
30
52
  id={`${uid}-tab-${tab.id}`}
31
53
  aria-selected={value === tab.id}
32
54
  aria-controls={`${uid}-panel-${tab.id}`}
55
+ tabindex={value === tab.id ? 0 : -1}
33
56
  class="terminoir-tabs__trigger terminoir-focusable"
34
57
  onclick={() => (value = tab.id)}
35
58
  >
59
+ {#if tab.icon}<NavIcon name={tab.icon} />{/if}
36
60
  {tab.label}
37
61
  </button>
38
62
  {/each}
@@ -65,6 +89,9 @@
65
89
  }
66
90
 
67
91
  .terminoir-tabs__trigger {
92
+ display: inline-flex;
93
+ align-items: center;
94
+ gap: var(--space-2);
68
95
  flex-shrink: 0;
69
96
  white-space: nowrap;
70
97
  border: 0;
@@ -85,4 +112,34 @@
85
112
  .terminoir-tabs__panel {
86
113
  padding-top: var(--space-3);
87
114
  }
115
+
116
+ /* Vertical: a column of tabs beside the panel, kept in view while the panel scrolls. */
117
+ @media (min-width: 701px) {
118
+ .terminoir-tabs--vertical {
119
+ display: grid;
120
+ grid-template-columns: minmax(8rem, 11rem) minmax(0, 1fr);
121
+ gap: var(--space-5);
122
+ align-items: start;
123
+ }
124
+ .terminoir-tabs--vertical > .terminoir-tabs__list {
125
+ display: flex;
126
+ flex-direction: column;
127
+ position: sticky;
128
+ top: var(--space-3);
129
+ overflow: visible;
130
+ }
131
+ .terminoir-tabs--vertical .terminoir-tabs__trigger {
132
+ width: 100%;
133
+ text-align: left;
134
+ }
135
+ .terminoir-tabs--vertical .terminoir-tabs__trigger[aria-selected='true'] {
136
+ background: color-mix(in srgb, var(--primary) 14%, transparent);
137
+ box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--primary) 28%, var(--border));
138
+ }
139
+ .terminoir-tabs--vertical > .terminoir-tabs__panel {
140
+ grid-column: 2;
141
+ grid-row: 1;
142
+ padding-top: 0;
143
+ }
144
+ }
88
145
  </style>
@@ -1,10 +1,14 @@
1
1
  import type { Snippet } from 'svelte';
2
2
  type $$ComponentProps = {
3
+ /** `icon` is a Font Awesome name, as for `NavIcon`. */
3
4
  tabs: Array<{
4
5
  id: string;
5
6
  label: string;
7
+ icon?: string;
6
8
  }>;
7
9
  value?: string;
10
+ /** `vertical`: the tabs stack in a column beside the panel (a settings sidebar); a scrolling row on phones. */
11
+ orientation?: 'horizontal' | 'vertical';
8
12
  class?: string;
9
13
  children?: Snippet;
10
14
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "terminoir",
3
- "version": "0.18.2",
3
+ "version": "0.19.0",
4
4
  "description": "Terminoir — Liquid Glass design tokens and Svelte 5 UI components.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",