@streamscloud/kit 0.9.0 → 0.9.2

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 (37) hide show
  1. package/README.md +28 -0
  2. package/dist/styles/_semantic.scss +11 -11
  3. package/dist/ui/ai-panel/ai-panel-localization.d.ts +3 -0
  4. package/dist/ui/ai-panel/ai-panel-localization.js +12 -0
  5. package/dist/ui/ai-panel/cmp.ai-panel.svelte +90 -0
  6. package/dist/ui/ai-panel/cmp.ai-panel.svelte.d.ts +58 -0
  7. package/dist/ui/ai-panel/index.d.ts +1 -0
  8. package/dist/ui/ai-panel/index.js +1 -0
  9. package/dist/ui/avatar/cmp.avatar.svelte +1 -1
  10. package/dist/ui/button/cmp.button.svelte +3 -3
  11. package/dist/ui/button/cmp.button.svelte.d.ts +1 -1
  12. package/dist/ui/checkbox/cmp.checkbox.svelte +10 -6
  13. package/dist/ui/chip-group/cmp.chip-group.svelte +2 -1
  14. package/dist/ui/color-picker/cmp.color-picker.svelte +60 -1
  15. package/dist/ui/color-picker/cmp.color-picker.svelte.d.ts +6 -0
  16. package/dist/ui/date-picker/cmp.date-picker.svelte +1 -1
  17. package/dist/ui/input/cmp.input.svelte +1 -1
  18. package/dist/ui/numeral-input/cmp.numeral-input.svelte +1 -1
  19. package/dist/ui/option-pill/cmp.option-pill.svelte +2 -1
  20. package/dist/ui/page-layout/cmp.page-panel-sidebars.svelte +18 -0
  21. package/dist/ui/page-layout/cmp.page-panel-sidebars.svelte.d.ts +8 -1
  22. package/dist/ui/page-layout/cmp.page-panel.svelte +2 -0
  23. package/dist/ui/page-layout/cmp.panel-collapse-button.svelte +3 -2
  24. package/dist/ui/pin-input/cmp.pin-input.svelte +2 -1
  25. package/dist/ui/radio/cmp.radio-card.svelte +7 -2
  26. package/dist/ui/radio/cmp.radio.svelte +8 -2
  27. package/dist/ui/tabs/cmp.tabs.svelte +15 -102
  28. package/dist/ui/tabs/cmp.tabs.svelte.d.ts +13 -10
  29. package/dist/ui/tabs/index.d.ts +1 -1
  30. package/dist/ui/tabs/tab.svelte +169 -0
  31. package/dist/ui/tabs/tab.svelte.d.ts +34 -0
  32. package/dist/ui/tabs/tabs-localization.d.ts +3 -0
  33. package/dist/ui/tabs/tabs-localization.js +12 -0
  34. package/dist/ui/tabs/types.d.ts +8 -0
  35. package/dist/ui/tabs/types.js +1 -0
  36. package/dist/ui/toggle/cmp.toggle.svelte +2 -2
  37. package/package.json +5 -2
package/README.md ADDED
@@ -0,0 +1,28 @@
1
+ # @streamscloud/kit
2
+
3
+ Shared UI components + utilities for StreamsCloud frontend apps. Svelte 5 (runes), TypeScript strict, SCSS/BEM.
4
+
5
+ ## Develop
6
+
7
+ ```sh
8
+ npm install
9
+ npm run dev # dev playground, http://localhost:3011
10
+ ```
11
+
12
+ After cloning, run `./setup-claude.sh` (Windows: `.\setup-claude.ps1`) to wire shared Claude hooks and skills from the sibling `development-memory` checkout —
13
+ without it agent sessions run with no hooks and no shared skills. Re-run after changing `.claude/sc-skills`.
14
+
15
+ ## Checks
16
+
17
+ ```sh
18
+ npm run check # svelte-check + tsc for node files
19
+ npm run lint # prettier --check + eslint
20
+ ```
21
+
22
+ ## Release
23
+
24
+ Always via the npm scripts — `npm run publish:prod` / `npm run publish:dev`, never raw `npm publish`. The full workflow is in `.claude/CLAUDE.md`.
25
+
26
+ ## Context
27
+
28
+ Project facts, decisions (ADRs), and reference live in [`project-dev-memory/`](project-dev-memory/README.md). Agent rules live in `.claude/`.
@@ -7,11 +7,11 @@
7
7
  // ============================================================
8
8
  // COLOR — SURFACES
9
9
  // ============================================================
10
- --sc-kit--color--bg--app: light-dark(#f6f7f9, #{$color-dark-900});
10
+ --sc-kit--color--bg--app: light-dark(#{$color-neutral-50}, #{$color-dark-900});
11
11
  --sc-kit--color--bg--panel: light-dark(#{$color-white}, #{$color-dark-800});
12
- --sc-kit--color--bg--element: light-dark(#{$color-neutral-50}, #{$color-dark-800});
12
+ --sc-kit--color--bg--element: light-dark(#{$color-neutral-100}, #{$color-dark-700});
13
13
  --sc-kit--color--bg--field: light-dark(#{$color-white}, #{$color-dark-700});
14
- --sc-kit--color--bg--field-alt: light-dark(#f8f9fb, #{$color-dark-700});
14
+ --sc-kit--color--bg--field-alt: light-dark(#{$color-neutral-50}, #{$color-dark-700});
15
15
  --sc-kit--color--bg--hover: light-dark(#f3f5f8, #{$color-dark-700});
16
16
  --sc-kit--color--bg--active: light-dark(#eef2f8, #{$color-dark-600});
17
17
  --sc-kit--color--bg--menu-active: light-dark(#{$color-blue-50}, #{$color-dark-500});
@@ -20,9 +20,9 @@
20
20
  // ============================================================
21
21
  // COLOR — TEXT
22
22
  // ============================================================
23
- --sc-kit--color--text--primary: light-dark(#1a1d21, #{$color-white});
23
+ --sc-kit--color--text--primary: light-dark(#{$color-dark-800}, #{$color-white});
24
24
  --sc-kit--color--text--secondary: light-dark(#5c6370, #{$color-neutral-300});
25
- --sc-kit--color--text--muted: light-dark(#9aa0aa, #{$color-neutral-400});
25
+ --sc-kit--color--text--muted: light-dark(#{$color-neutral-400}, #{$color-neutral-400});
26
26
  --sc-kit--color--text--placeholder: light-dark(#b4b9c2, #{$color-neutral-500});
27
27
  --sc-kit--color--text--label: light-dark(#{$color-neutral-500}, #{$color-neutral-300});
28
28
  --sc-kit--color--text--on-accent: #{$color-white};
@@ -31,17 +31,17 @@
31
31
  // COLOR — BORDERS
32
32
  // ============================================================
33
33
  --sc-kit--color--border: light-dark(#{$color-neutral-200}, #{$color-dark-600});
34
- --sc-kit--color--border--strong: light-dark(#d5d8de, #{$color-dark-500});
35
- --sc-kit--color--border--field: light-dark(#e3e6eb, #{$color-dark-600});
34
+ --sc-kit--color--border--strong: light-dark(#{$color-neutral-400}, #{$color-neutral-600});
35
+ --sc-kit--color--border--field: light-dark(#{$color-neutral-200}, #{$color-dark-600});
36
36
  --sc-kit--color--border--focus: light-dark(#{$color-blue-500}, #{$color-blue-400});
37
37
 
38
38
  // ============================================================
39
39
  // COLOR — BRAND / ACCENT — anchored at #144AB0
40
40
  // ============================================================
41
41
  --sc-kit--color--accent: light-dark(#{$color-blue-500}, #{$color-blue-400});
42
- --sc-kit--color--accent--hover: light-dark(#0f3a8c, #{$color-blue-500});
43
- --sc-kit--color--accent--soft: light-dark(#e3ebf8, #{$color-blue-900});
44
- --sc-kit--color--accent--softer: light-dark(#f1f5fb, #{$color-blue-900});
42
+ --sc-kit--color--accent--hover: light-dark(#{$color-blue-700}, #{$color-blue-500});
43
+ --sc-kit--color--accent--soft: light-dark(#{$color-blue-100}, #{$color-blue-900});
44
+ --sc-kit--color--accent--softer: light-dark(#{$color-blue-50}, #{$color-blue-900});
45
45
 
46
46
  // ============================================================
47
47
  // COLOR — STATUS
@@ -65,7 +65,7 @@
65
65
  // COLOR — OVERLAYS
66
66
  // ============================================================
67
67
  --sc-kit--color--bg--scrim: light-dark(rgb(0 0 0 / 45%), rgb(0 0 0 / 65%));
68
- --sc-kit--color--bg--tooltip: light-dark(#1a1d21, #{$color-dark-700});
68
+ --sc-kit--color--bg--tooltip: light-dark(#{$color-dark-800}, #{$color-dark-700});
69
69
  --sc-kit--color--text--tooltip: #{$color-white};
70
70
 
71
71
  // Z-scale — relative stacking order for the library's overlay components.
@@ -0,0 +1,3 @@
1
+ export declare class AiPanelLocalization {
2
+ get attach(): string;
3
+ }
@@ -0,0 +1,12 @@
1
+ import { AppLocale } from '../../core/locale';
2
+ const loc = {
3
+ attach: {
4
+ en: 'Attach',
5
+ no: 'Legg ved'
6
+ }
7
+ };
8
+ export class AiPanelLocalization {
9
+ get attach() {
10
+ return loc.attach[AppLocale.current];
11
+ }
12
+ }
@@ -0,0 +1,90 @@
1
+ <script lang="ts" generics="T">import { Button } from '../button';
2
+ import { SegmentedControl } from '../segmented-control';
3
+ import { Textarea } from '../textarea';
4
+ import { AiPanelLocalization } from './ai-panel-localization';
5
+ import IconAdd from '@fluentui/svg-icons/icons/add_20_regular.svg?raw';
6
+ import IconSparkle from '@fluentui/svg-icons/icons/sparkle_20_regular.svg?raw';
7
+ const { segments, activeSegment, body, prompt = '', placeholder = '', generateLabel = '', on } = $props();
8
+ const localization = new AiPanelLocalization();
9
+ </script>
10
+
11
+ <div class="ai-panel">
12
+ <div class="ai-panel__tabs">
13
+ <SegmentedControl value={activeSegment} segments={segments} size="sm" fullWidth on={{ change: (value) => on?.tabChange?.(value) }} />
14
+ </div>
15
+
16
+ <div class="ai-panel__body">
17
+ {@render body()}
18
+ </div>
19
+
20
+ <div class="ai-panel__composer">
21
+ <Textarea value={prompt} placeholder={placeholder} borderless rows={3} on={{ input: (value) => on?.promptInput?.(value) }} />
22
+ <span class="ai-panel__attach">
23
+ <Button type="button" variant="ghost" size="sm" icon={IconAdd} aria-label={localization.attach} on={{ click: () => on?.attach?.() }} />
24
+ </span>
25
+ </div>
26
+
27
+ <Button type="button" variant="success" fullWidth icon={IconSparkle} on={{ click: () => on?.generate?.() }}>{generateLabel}</Button>
28
+ </div>
29
+
30
+ <!--
31
+ @component
32
+ AiPanel — right-edge AI assistant panel: a segment strip, a consumer-filled `body`, and a built-in
33
+ composer (prompt textarea + attach button + Generate bar). Compose-only; collapse is the surrounding
34
+ layout's concern. User-facing copy (placeholder, Generate label) is consumer-supplied.
35
+
36
+ ### CSS Custom Properties
37
+ | Property | Description | Default |
38
+ |---|---|---|
39
+ | `--sc-kit--ai-panel--width` | Panel width | `300px` |
40
+ | `--sc-kit--ai-panel--gap` | Gap between sections | `16px` |
41
+ | `--sc-kit--ai-panel--padding` | Panel padding | `16px` |
42
+ | `--sc-kit--ai-panel--background` | Panel background | `--sc-kit--color--bg--panel` |
43
+ | `--sc-kit--ai-panel--border-color` | Left divider color | `--sc-kit--color--border` |
44
+ | `--sc-kit--ai-panel--surface` | Body / composer / tab-tray fill | `--sc-kit--color--bg--app` |
45
+ | `--sc-kit--ai-panel--surface-radius` | Body / composer corner radius | `--sc-kit--radius--md` |
46
+ -->
47
+
48
+ <style>.ai-panel {
49
+ --_ai-panel--width: var(--sc-kit--ai-panel--width, 18.75rem);
50
+ --_ai-panel--gap: var(--sc-kit--ai-panel--gap, 1rem);
51
+ --_ai-panel--padding: var(--sc-kit--ai-panel--padding, 1rem);
52
+ --_ai-panel--background: var(--sc-kit--ai-panel--background, var(--sc-kit--color--bg--panel));
53
+ --_ai-panel--border-color: var(--sc-kit--ai-panel--border-color, var(--sc-kit--color--border));
54
+ --_ai-panel--surface: var(--sc-kit--ai-panel--surface, var(--sc-kit--color--bg--app));
55
+ --_ai-panel--surface-radius: var(--sc-kit--ai-panel--surface-radius, var(--sc-kit--radius--md));
56
+ display: flex;
57
+ flex-direction: column;
58
+ gap: var(--_ai-panel--gap);
59
+ width: var(--_ai-panel--width);
60
+ padding: var(--_ai-panel--padding);
61
+ background: var(--_ai-panel--background);
62
+ border-left: 1px solid var(--_ai-panel--border-color);
63
+ overflow: hidden;
64
+ }
65
+ .ai-panel__tabs {
66
+ --sc-kit--segmented-control--tray--background: var(--_ai-panel--surface);
67
+ margin-bottom: -0.5rem;
68
+ }
69
+ .ai-panel__body {
70
+ flex: 1;
71
+ min-height: 15rem;
72
+ background: var(--_ai-panel--surface);
73
+ border-radius: var(--_ai-panel--surface-radius);
74
+ overflow-y: auto;
75
+ }
76
+ .ai-panel__composer {
77
+ position: relative;
78
+ display: flex;
79
+ flex-direction: column;
80
+ gap: 0.5rem;
81
+ min-height: 6rem;
82
+ padding: 0.75rem;
83
+ background: var(--_ai-panel--surface);
84
+ border-radius: var(--_ai-panel--surface-radius);
85
+ }
86
+ .ai-panel__attach {
87
+ position: absolute;
88
+ bottom: 0.5rem;
89
+ left: 0.5rem;
90
+ }</style>
@@ -0,0 +1,58 @@
1
+ import { type Segment } from '../segmented-control';
2
+ import type { Snippet } from 'svelte';
3
+ declare function $$render<T>(): {
4
+ props: {
5
+ segments: Segment<NoInfer<T>>[];
6
+ activeSegment: T;
7
+ /** Content for the active segment — the consumer renders per-segment UI here. */
8
+ body: Snippet;
9
+ /** Composer textarea value. */
10
+ prompt?: string;
11
+ placeholder?: string;
12
+ /** Generate button label — consumer-supplied (kit ships no user-facing copy). */
13
+ generateLabel?: string;
14
+ on?: {
15
+ tabChange?: (value: T) => void;
16
+ promptInput?: (value: string) => void;
17
+ attach?: () => void;
18
+ generate?: () => void;
19
+ };
20
+ };
21
+ exports: {};
22
+ bindings: "";
23
+ slots: {};
24
+ events: {};
25
+ };
26
+ declare class __sveltets_Render<T> {
27
+ props(): ReturnType<typeof $$render<T>>['props'];
28
+ events(): ReturnType<typeof $$render<T>>['events'];
29
+ slots(): ReturnType<typeof $$render<T>>['slots'];
30
+ bindings(): "";
31
+ exports(): {};
32
+ }
33
+ interface $$IsomorphicComponent {
34
+ new <T>(options: import('svelte').ComponentConstructorOptions<ReturnType<__sveltets_Render<T>['props']>>): import('svelte').SvelteComponent<ReturnType<__sveltets_Render<T>['props']>, ReturnType<__sveltets_Render<T>['events']>, ReturnType<__sveltets_Render<T>['slots']>> & {
35
+ $$bindings?: ReturnType<__sveltets_Render<T>['bindings']>;
36
+ } & ReturnType<__sveltets_Render<T>['exports']>;
37
+ <T>(internal: unknown, props: ReturnType<__sveltets_Render<T>['props']> & {}): ReturnType<__sveltets_Render<T>['exports']>;
38
+ z_$$bindings?: ReturnType<__sveltets_Render<any>['bindings']>;
39
+ }
40
+ /**
41
+ * AiPanel — right-edge AI assistant panel: a segment strip, a consumer-filled `body`, and a built-in
42
+ * composer (prompt textarea + attach button + Generate bar). Compose-only; collapse is the surrounding
43
+ * layout's concern. User-facing copy (placeholder, Generate label) is consumer-supplied.
44
+ *
45
+ * ### CSS Custom Properties
46
+ * | Property | Description | Default |
47
+ * |---|---|---|
48
+ * | `--sc-kit--ai-panel--width` | Panel width | `300px` |
49
+ * | `--sc-kit--ai-panel--gap` | Gap between sections | `16px` |
50
+ * | `--sc-kit--ai-panel--padding` | Panel padding | `16px` |
51
+ * | `--sc-kit--ai-panel--background` | Panel background | `--sc-kit--color--bg--panel` |
52
+ * | `--sc-kit--ai-panel--border-color` | Left divider color | `--sc-kit--color--border` |
53
+ * | `--sc-kit--ai-panel--surface` | Body / composer / tab-tray fill | `--sc-kit--color--bg--app` |
54
+ * | `--sc-kit--ai-panel--surface-radius` | Body / composer corner radius | `--sc-kit--radius--md` |
55
+ */
56
+ declare const Cmp: $$IsomorphicComponent;
57
+ type Cmp<T> = InstanceType<typeof Cmp<T>>;
58
+ export default Cmp;
@@ -0,0 +1 @@
1
+ export { default as AiPanel } from './cmp.ai-panel.svelte';
@@ -0,0 +1 @@
1
+ export { default as AiPanel } from './cmp.ai-panel.svelte';
@@ -62,7 +62,7 @@ the load / error / stub state machine.
62
62
  --sc-kit--image--border-radius: 50%;
63
63
  --sc-kit--image--background: var(--_avatar--background);
64
64
  position: relative;
65
- display: inline-flex;
65
+ display: flex;
66
66
  align-items: center;
67
67
  justify-content: center;
68
68
  width: var(--_avatar--size);
@@ -105,7 +105,7 @@ Pass `type="anchor"` to render as `<a>` with `href`. Otherwise `type` is the nat
105
105
  | `--sc-kit--button--gap` | Gap between icon and label | per size |
106
106
  | `--sc-kit--button--border-radius` | Corner rounding | `var(--sc-kit--radius--md)` |
107
107
  | `--sc-kit--button--font-weight` | Font weight | `var(--sc-kit--font-weight--medium)` |
108
- | `--sc-kit--button--width` | Explicit width | `auto` |
108
+ | `--sc-kit--button--width` | Explicit width | `fit-content` |
109
109
  | `--sc-kit--button--min-width` | Minimum width | `0` |
110
110
  | `--sc-kit--button--max-width` | Maximum width | `100%` |
111
111
  -->
@@ -122,13 +122,13 @@ Pass `type="anchor"` to render as `<a>` with `href`. Otherwise `type` is the nat
122
122
  --_btn--padding-inline: var(--sc-kit--button--padding-inline, var(--sc-kit--space--4));
123
123
  --_btn--font-size: var(--sc-kit--button--font-size, var(--sc-kit--font-size--md));
124
124
  --_btn--gap: var(--sc-kit--button--gap, var(--sc-kit--space--2));
125
- --_btn--width: var(--sc-kit--button--width, auto);
125
+ --_btn--width: var(--sc-kit--button--width, fit-content);
126
126
  --_btn--min-width: var(--sc-kit--button--min-width, 0);
127
127
  --_btn--max-width: var(--sc-kit--button--max-width, 100%);
128
128
  --_btn--border-radius: var(--sc-kit--button--border-radius, var(--sc-kit--radius--md));
129
129
  --_btn--font-weight: var(--sc-kit--button--font-weight, var(--sc-kit--font-weight--medium));
130
130
  box-sizing: border-box;
131
- display: inline-flex;
131
+ display: flex;
132
132
  align-items: center;
133
133
  justify-content: center;
134
134
  gap: var(--_btn--gap);
@@ -63,7 +63,7 @@ type Props = ButtonModeProps | AnchorModeProps;
63
63
  * | `--sc-kit--button--gap` | Gap between icon and label | per size |
64
64
  * | `--sc-kit--button--border-radius` | Corner rounding | `var(--sc-kit--radius--md)` |
65
65
  * | `--sc-kit--button--font-weight` | Font weight | `var(--sc-kit--font-weight--medium)` |
66
- * | `--sc-kit--button--width` | Explicit width | `auto` |
66
+ * | `--sc-kit--button--width` | Explicit width | `fit-content` |
67
67
  * | `--sc-kit--button--min-width` | Minimum width | `0` |
68
68
  * | `--sc-kit--button--max-width` | Maximum width | `100%` |
69
69
  */
@@ -17,11 +17,9 @@ const handleClick = (event) => {
17
17
 
18
18
  <label class="check" class:check--on={isOn} class:check--indeterminate={isIndeterminate} class:check--disabled={disabled} title={title}>
19
19
  <button type="button" class="check__box" role="checkbox" aria-checked={ariaChecked} disabled={disabled} onclick={handleClick}>
20
- {#if isOn}
21
- <span class="check__mark" aria-hidden="true"><Icon src={IconCheckmark} /></span>
22
- {:else if isIndeterminate}
23
- <span class="check__mark" aria-hidden="true"><Icon src={IconSubtract} /></span>
24
- {/if}
20
+ <span class="check__mark" class:check__mark--visible={isOn || isIndeterminate} aria-hidden="true">
21
+ <Icon src={isIndeterminate ? IconSubtract : IconCheckmark} />
22
+ </span>
25
23
  </button>
26
24
  {#if typeof label === 'string'}
27
25
  <span class="check__label">{label}</span>
@@ -66,9 +64,10 @@ so it can be safely nested inside clickable rows.
66
64
  --_check--label-font-size: var(--sc-kit--checkbox--label--font-size, var(--sc-kit--font-size--md));
67
65
  --_check--label-line-height: var(--sc-kit--checkbox--label--line-height, var(--sc-kit--line-height--md));
68
66
  --_check--label-color: var(--sc-kit--checkbox--label--color, var(--sc-kit--color--text--primary));
69
- display: inline-flex;
67
+ display: flex;
70
68
  align-items: center;
71
69
  gap: var(--_check--gap);
70
+ width: fit-content;
72
71
  max-width: 100%;
73
72
  cursor: pointer;
74
73
  user-select: none;
@@ -110,6 +109,11 @@ so it can be safely nested inside clickable rows.
110
109
  .check__mark {
111
110
  --sc-kit--icon--size: 0.75rem;
112
111
  display: inline-flex;
112
+ opacity: 0;
113
+ transition: opacity var(--sc-kit--duration--base) var(--sc-kit--ease--default);
114
+ }
115
+ .check__mark--visible {
116
+ opacity: 1;
113
117
  }
114
118
  .check__label {
115
119
  min-width: 0;
@@ -85,9 +85,10 @@ checkmark on selected chips.
85
85
  --_cg--color-selected: var(--sc-kit--chip-group--color--selected, var(--sc-kit--color--accent));
86
86
  --_cg--background-selected-hover: var(--sc-kit--chip-group--background--selected-hover, var(--sc-kit--color--accent--softer));
87
87
  --_cg--focus-ring-color: var(--sc-kit--chip-group--focus-ring-color, var(--sc-kit--color--border--focus));
88
- display: inline-flex;
88
+ display: flex;
89
89
  flex-wrap: wrap;
90
90
  gap: var(--_cg--gap);
91
+ width: fit-content;
91
92
  max-width: 100%;
92
93
  }
93
94
  .chip-group--sm {
@@ -8,7 +8,7 @@ import IconColorF from '@fluentui/svg-icons/icons/color_20_regular.svg?raw';
8
8
  import IconDismiss from '@fluentui/svg-icons/icons/dismiss_20_regular.svg?raw';
9
9
  import { colord } from 'colord';
10
10
  import AwesomeColorPicker from 'svelte-awesome-color-picker';
11
- let { value, size = 'md', defaultColor, clearable = false, disabled = false, placeholder, on, children, previewIcon } = $props();
11
+ let { value, variant = 'fill', label, size = 'md', defaultColor, clearable = false, disabled = false, placeholder, on, children, previewIcon } = $props();
12
12
  const loc = new ColorPickerLocalization();
13
13
  const displayColor = $derived(value || defaultColor || '');
14
14
  const textColor = $derived.by(() => {
@@ -33,6 +33,7 @@ const previewBackgroundColor = $derived.by(() => {
33
33
  }
34
34
  return colord(displayColor).toHex().substring(0, 7);
35
35
  });
36
+ const alphaPercent = $derived(displayColor ? `${Math.round(colord(displayColor).alpha() * 100)}%` : '');
36
37
  const triggerPlaceholder = $derived(placeholder ?? loc.notSet);
37
38
  const resetValue = (e) => {
38
39
  e.preventDefault();
@@ -52,6 +53,14 @@ const onColorPickerInput = ({ hex }) => {
52
53
  <div class="color-picker__trigger">
53
54
  {#if children}
54
55
  {@render children()}
56
+ {:else if variant === 'field'}
57
+ <div class="color-picker__field color-picker__field--{size}">
58
+ <span class="color-picker__field-swatch" style:background={previewBackgroundColor} aria-hidden="true"></span>
59
+ <span class="color-picker__field-label">{label}</span>
60
+ {#if displayColor}
61
+ <span class="color-picker__field-value">{alphaPercent}</span>
62
+ {/if}
63
+ </div>
55
64
  {:else}
56
65
  <Input
57
66
  value={value || ''}
@@ -118,6 +127,8 @@ between black and white for contrast.
118
127
  | Prop | Type | Default |
119
128
  |---|---|---|
120
129
  | `value` | `string \| null \| undefined` | required |
130
+ | `variant` | `'fill' \| 'field'` | `'fill'` |
131
+ | `label` | `string` (field variant row label) | — |
121
132
  | `size` | `'sm' \| 'md' \| 'lg'` | `'md'` |
122
133
  | `clearable` | `boolean` | `false` |
123
134
  | `disabled` | `boolean` | `false` |
@@ -147,6 +158,54 @@ between black and white for contrast.
147
158
  transition: none;
148
159
  --sc-kit--input--cursor--inert: pointer;
149
160
  }
161
+ .color-picker__field {
162
+ --_color-picker--field-height: var(--sc-kit--field--height--md);
163
+ --_color-picker--field-padding-inline: var(--sc-kit--field--padding-inline--md);
164
+ display: flex;
165
+ width: 100%;
166
+ align-items: center;
167
+ gap: var(--sc-kit--space--2);
168
+ height: var(--_color-picker--field-height);
169
+ padding-inline: var(--_color-picker--field-padding-inline);
170
+ background: var(--sc-kit--color--bg--field);
171
+ border: 1px solid var(--sc-kit--color--border--field);
172
+ border-radius: var(--sc-kit--radius--md);
173
+ cursor: pointer;
174
+ transition: border-color var(--sc-kit--duration--base) var(--sc-kit--ease--default), background-color var(--sc-kit--duration--base) var(--sc-kit--ease--default);
175
+ }
176
+ .color-picker__field--sm {
177
+ --_color-picker--field-height: var(--sc-kit--field--height--sm);
178
+ --_color-picker--field-padding-inline: var(--sc-kit--field--padding-inline--sm);
179
+ }
180
+ .color-picker__field--lg {
181
+ --_color-picker--field-height: var(--sc-kit--field--height--lg);
182
+ --_color-picker--field-padding-inline: var(--sc-kit--field--padding-inline--lg);
183
+ }
184
+ .color-picker__field:hover {
185
+ border-color: var(--sc-kit--color--border--strong);
186
+ }
187
+ .color-picker__field-swatch {
188
+ width: 1.25rem;
189
+ height: 1.25rem;
190
+ flex-shrink: 0;
191
+ border: 1px solid var(--sc-kit--color--border);
192
+ border-radius: var(--sc-kit--radius--sm);
193
+ }
194
+ .color-picker__field-label {
195
+ flex: 1;
196
+ min-width: 0;
197
+ color: var(--sc-kit--color--text--primary);
198
+ font-size: var(--sc-kit--font-size--sm);
199
+ text-overflow: ellipsis;
200
+ max-width: 100%;
201
+ white-space: nowrap;
202
+ overflow: hidden;
203
+ }
204
+ .color-picker__field-value {
205
+ flex-shrink: 0;
206
+ color: var(--sc-kit--color--text--secondary);
207
+ font-size: var(--sc-kit--font-size--sm);
208
+ }
150
209
  .color-picker__chevron, .color-picker__clear {
151
210
  display: inline-flex;
152
211
  align-items: center;
@@ -1,6 +1,10 @@
1
1
  import type { Snippet } from 'svelte';
2
2
  type Props = {
3
3
  value: string | null | undefined;
4
+ /** Trigger presentation: `'fill'` floods the field with the color; `'field'` is a compact row (swatch + label + opacity). @default 'fill' */
5
+ variant?: 'fill' | 'field';
6
+ /** Row label for the `'field'` variant (e.g. "Background Color"). */
7
+ label?: string;
4
8
  /** Size preset (shared with Input / DatePicker / Select). @default 'md' */
5
9
  size?: 'sm' | 'md' | 'lg';
6
10
  /** Default color shown in the picker UI when value is empty (does not set the value itself). */
@@ -35,6 +39,8 @@ type Props = {
35
39
  * | Prop | Type | Default |
36
40
  * |---|---|---|
37
41
  * | `value` | `string \| null \| undefined` | required |
42
+ * | `variant` | `'fill' \| 'field'` | `'fill'` |
43
+ * | `label` | `string` (field variant row label) | — |
38
44
  * | `size` | `'sm' \| 'md' \| 'lg'` | `'md'` |
39
45
  * | `clearable` | `boolean` | `false` |
40
46
  * | `disabled` | `boolean` | `false` |
@@ -222,7 +222,7 @@ DatePicker — single-day calendar picker built on Floating UI for positioning.
222
222
  }
223
223
  .date-picker {
224
224
  position: relative;
225
- display: inline-block;
225
+ display: flex;
226
226
  width: var(--_dp--width);
227
227
  max-width: 100%;
228
228
  }
@@ -195,7 +195,7 @@ or the clear button (see `HandleInput`'s availability pill).
195
195
  }
196
196
  .input {
197
197
  box-sizing: border-box;
198
- display: inline-flex;
198
+ display: flex;
199
199
  align-items: center;
200
200
  width: var(--_in--width);
201
201
  max-width: 100%;
@@ -232,7 +232,7 @@ requires a non-null value. CSS API mirrors `Input` for visual-language compatibi
232
232
  }
233
233
  .numeral-input {
234
234
  box-sizing: border-box;
235
- display: inline-flex;
235
+ display: flex;
236
236
  align-items: center;
237
237
  width: var(--_ni--width);
238
238
  max-width: 100%;
@@ -77,9 +77,10 @@ or `type="multi"` with `value: T[]`. Provide `compare` for non-primitive values.
77
77
  --_op--thumb-size: var(--sc-kit--option-pill--thumb--size, 1.75rem);
78
78
  --_op--thumb-background: var(--sc-kit--option-pill--thumb--background, var(--sc-kit--color--bg--active));
79
79
  --_op--thumb-color: var(--sc-kit--option-pill--thumb--color, var(--sc-kit--color--text--muted));
80
- display: inline-flex;
80
+ display: flex;
81
81
  flex-wrap: wrap;
82
82
  gap: var(--_op--gap);
83
+ width: fit-content;
83
84
  }
84
85
  .option-pill--disabled {
85
86
  opacity: 0.6;
@@ -31,6 +31,21 @@ $effect(() => {
31
31
  const collapseButtons = $derived([leftPanel?.id, rightPanel?.id].filter((id) => id !== null && id !== undefined));
32
32
  const leftCollapsed = $derived(leftPanel?.entry.collapsed ?? false);
33
33
  const rightCollapsed = $derived(rightPanel?.entry.collapsed ?? false);
34
+ export function toggleLeft() {
35
+ leftPanel?.entry.toggleCollapsed();
36
+ }
37
+ export function toggleRight() {
38
+ rightPanel?.entry.toggleCollapsed();
39
+ }
40
+ /** Collapses both sidebars, or expands both when they are already all collapsed. */
41
+ export function toggleBoth() {
42
+ const entries = [leftPanel?.entry, rightPanel?.entry].filter((entry) => !!entry);
43
+ if (entries.length === 0) {
44
+ return;
45
+ }
46
+ const allCollapsed = entries.every((entry) => entry.collapsed);
47
+ entries.forEach((entry) => entry.setCollapsed(!allCollapsed));
48
+ }
34
49
  $effect(() => {
35
50
  if (leftPanel) {
36
51
  leftPanel.entry.isValid = sidebarLeftIsValid;
@@ -86,6 +101,9 @@ PagePanelSidebars — a `PagePanel` whose body has collapsible left / right side
86
101
  fictional left/right panels in the shared `PanelState` so the standard collapse buttons in the
87
102
  header can target them. Sidebars slide horizontally on collapse / expand.
88
103
 
104
+ Exposes imperative `toggleLeft()` / `toggleRight()` / `toggleBoth()` via `bind:this` for external
105
+ collapse controls (e.g. a top-bar button), so callers don't reach into `PanelState` by id.
106
+
89
107
  ### CSS Custom Properties
90
108
  | Property | Description | Default |
91
109
  |---|---|---|
@@ -20,11 +20,18 @@ type Props = {
20
20
  * fictional left/right panels in the shared `PanelState` so the standard collapse buttons in the
21
21
  * header can target them. Sidebars slide horizontally on collapse / expand.
22
22
  *
23
+ * Exposes imperative `toggleLeft()` / `toggleRight()` / `toggleBoth()` via `bind:this` for external
24
+ * collapse controls (e.g. a top-bar button), so callers don't reach into `PanelState` by id.
25
+ *
23
26
  * ### CSS Custom Properties
24
27
  * | Property | Description | Default |
25
28
  * |---|---|---|
26
29
  * | `--sc-kit--page-panel-sidebars--border-color` | Sidebar divider color | `--sc-kit--color--border` |
27
30
  */
28
- declare const Cmp: import("svelte").Component<Props, {}, "">;
31
+ declare const Cmp: import("svelte").Component<Props, {
32
+ toggleLeft: () => void;
33
+ toggleRight: () => void;
34
+ toggleBoth: () => void;
35
+ }, "">;
29
36
  type Cmp = ReturnType<typeof Cmp>;
30
37
  export default Cmp;
@@ -280,6 +280,7 @@ to min / max; Enter / Space toggle collapsed.
280
280
  }
281
281
  .page-panel__header-left {
282
282
  display: flex;
283
+ align-self: stretch;
283
284
  align-items: center;
284
285
  gap: 1rem;
285
286
  min-width: 0;
@@ -298,6 +299,7 @@ to min / max; Enter / Space toggle collapsed.
298
299
  }
299
300
  .page-panel__header-right {
300
301
  display: flex;
302
+ align-self: stretch;
301
303
  align-items: center;
302
304
  justify-content: flex-end;
303
305
  margin-left: auto;
@@ -1,6 +1,6 @@
1
1
  <script lang="ts">import { Icon } from '../icon';
2
2
  import { PageLayoutLocalization } from './page-layout-localization';
3
- import IconSlideTransition from '@fluentui/svg-icons/icons/slide_transition_20_regular.svg?raw';
3
+ import IconPanelLeft from '@fluentui/svg-icons/icons/panel_left_20_regular.svg?raw';
4
4
  const { panelState, panelId } = $props();
5
5
  const localization = new PageLayoutLocalization();
6
6
  const entry = $derived(panelState.get(panelId));
@@ -19,7 +19,7 @@ const handleClick = () => {
19
19
  onclick={handleClick}
20
20
  aria-label={localization.togglePanel}
21
21
  aria-pressed={entry.collapsed}>
22
- <Icon src={IconSlideTransition} />
22
+ <Icon src={IconPanelLeft} />
23
23
  </button>
24
24
  {/if}
25
25
 
@@ -38,6 +38,7 @@ state. Renders nothing if the target panel isn't registered. Typically rendered
38
38
  | `--sc-kit--panel-collapse-button--color--invalid` | Color when the target panel is invalid | `--sc-kit--color--danger` |
39
39
  -->
40
40
  <style>.panel-collapse-button {
41
+ --sc-kit--icon--size: 1rem;
41
42
  --_panel-collapse-button--color: var(--sc-kit--panel-collapse-button--color, var(--sc-kit--color--text--muted));
42
43
  --_panel-collapse-button--color-hover: var(--sc-kit--panel-collapse-button--color--hover, var(--sc-kit--color--text--primary));
43
44
  --_panel-collapse-button--color-invalid: var(--sc-kit--panel-collapse-button--color--invalid, var(--sc-kit--color--danger));
@@ -116,8 +116,9 @@ as `<input type="password">`). Fires `on.complete` when all cells are filled.
116
116
  --_pi--border-radius: var(--sc-kit--pin-input--border-radius, var(--sc-kit--radius--lg));
117
117
  --_pi--color: var(--sc-kit--pin-input--color, var(--sc-kit--color--text--primary));
118
118
  --_pi--focus-shadow: var(--sc-kit--pin-input--focus-shadow, var(--sc-kit--shadow--focus));
119
- display: inline-flex;
119
+ display: flex;
120
120
  gap: var(--_pi--gap);
121
+ width: fit-content;
121
122
  }
122
123
  .pin-input--disabled {
123
124
  opacity: 0.6;
@@ -19,7 +19,7 @@ const handleClick = (event) => {
19
19
  disabled={disabled}
20
20
  onclick={handleClick}>
21
21
  <span class="radio-card__dot" aria-hidden="true">
22
- {#if selected}<span class="radio-card__inner"></span>{/if}
22
+ <span class="radio-card__inner" class:radio-card__inner--visible={selected}></span>
23
23
  </span>
24
24
  <span class="radio-card__body">
25
25
  <span class="radio-card__title">{title}</span>
@@ -78,7 +78,7 @@ label line (plan picker, account type, theme picker).
78
78
  --_card--dot-background: var(--sc-kit--radio-card--dot--background, var(--sc-kit--color--bg--field));
79
79
  --_card--dot-inner-color: var(--sc-kit--radio-card--dot--inner-color, var(--sc-kit--color--accent));
80
80
  box-sizing: border-box;
81
- display: inline-flex;
81
+ display: flex;
82
82
  align-items: flex-start;
83
83
  gap: var(--_card--gap);
84
84
  width: 100%;
@@ -127,6 +127,11 @@ label line (plan picker, account type, theme picker).
127
127
  height: var(--_card--dot-inner-size);
128
128
  background: var(--_card--dot-inner-color);
129
129
  border-radius: var(--sc-kit--radius--circle);
130
+ opacity: 0;
131
+ transition: opacity var(--sc-kit--duration--base) var(--sc-kit--ease--default);
132
+ }
133
+ .radio-card__inner--visible {
134
+ opacity: 1;
130
135
  }
131
136
  .radio-card__body {
132
137
  display: inline-flex;
@@ -12,7 +12,7 @@ export {};
12
12
 
13
13
  <label class="radio" class:radio--on={selected} class:radio--disabled={disabled} title={title}>
14
14
  <button type="button" class="radio__dot" role="radio" aria-checked={selected} aria-label={ariaLabel} disabled={disabled} onclick={handleClick}>
15
- {#if selected}<span class="radio__inner" aria-hidden="true"></span>{/if}
15
+ <span class="radio__inner" class:radio__inner--visible={selected} aria-hidden="true"></span>
16
16
  </button>
17
17
  {#if typeof label === 'string'}
18
18
  <span class="radio__label">{label}</span>
@@ -55,9 +55,10 @@ its state to this option's value. Pair with `RadioCard` for option-card layouts.
55
55
  --_radio--label-font-size: var(--sc-kit--radio--label--font-size, var(--sc-kit--font-size--md));
56
56
  --_radio--label-line-height: var(--sc-kit--radio--label--line-height, var(--sc-kit--line-height--md));
57
57
  --_radio--label-color: var(--sc-kit--radio--label--color, var(--sc-kit--color--text--primary));
58
- display: inline-flex;
58
+ display: flex;
59
59
  align-items: center;
60
60
  gap: var(--_radio--gap);
61
+ width: fit-content;
61
62
  max-width: 100%;
62
63
  cursor: pointer;
63
64
  user-select: none;
@@ -95,6 +96,11 @@ its state to this option's value. Pair with `RadioCard` for option-card layouts.
95
96
  height: var(--_radio--inner-size);
96
97
  background: var(--_radio--inner-color);
97
98
  border-radius: var(--sc-kit--radius--circle);
99
+ opacity: 0;
100
+ transition: opacity var(--sc-kit--duration--base) var(--sc-kit--ease--default);
101
+ }
102
+ .radio__inner--visible {
103
+ opacity: 1;
98
104
  }
99
105
  .radio__label {
100
106
  min-width: 0;
@@ -1,40 +1,24 @@
1
- <script lang="ts" module>export {};
2
- </script>
3
-
4
- <script lang="ts" generics="T">const { value, tabs, variant = 'default', size = 'md', compare, on } = $props();
1
+ <script lang="ts" generics="T">import TabItem from './tab.svelte';
2
+ const { value, tabs, variant = 'default', size = 'md', compare, on } = $props();
5
3
  const isActive = (tab) => (compare ? compare(value, tab.value) : value === tab.value);
6
- const handleClick = (tab) => {
7
- if (tab.disabled || isActive(tab)) {
8
- return;
9
- }
10
- on?.change?.(tab.value);
11
- };
12
4
  </script>
13
5
 
14
- <div class="tabs tabs--{variant} tabs--{size}" role="tablist">
6
+ <div class="tabs tabs--{variant}" role="tablist">
15
7
  {#each tabs as tab, i (i)}
16
- <button
17
- type="button"
18
- class="tabs__tab"
19
- class:tabs__tab--active={isActive(tab)}
20
- class:tabs__tab--disabled={tab.disabled}
21
- role="tab"
22
- aria-selected={isActive(tab)}
23
- disabled={tab.disabled}
24
- onclick={() => handleClick(tab)}>
25
- <span class="tabs__label">{tab.label}</span>
26
- </button>
8
+ <TabItem tab={tab} variant={variant} size={size} active={isActive(tab)} on={{ select: on?.change, close: on?.close }} />
27
9
  {/each}
28
10
  </div>
29
11
 
30
12
  <!--
31
13
  @component
32
14
  Tabs — header strip for switching between content panels. Panel rendering is consumer-controlled
33
- (switch on the active `value`). Two visual variants:
15
+ (switch on the active `value`). Three visual variants:
34
16
 
35
17
  - **`default`**: underline — a 2px accent bar travels under the active tab; the strip has a bottom border.
36
18
  - **`pills`**: filled — the active tab lifts as a card on an inset tray.
19
+ - **`document`**: browser-style tabs — flush, rounded-top; the active tab background matches the canvas so it reads as continuous with the content below.
37
20
 
21
+ Providing `on.close` renders a close affordance on each non-disabled tab (works in any variant).
38
22
  Each tab carries an optional `disabled` flag (non-interactive). For navigation, not single-choice
39
23
  form input — use `SegmentedControl` for the latter (it's a `radiogroup`; this is a `tablist`).
40
24
 
@@ -43,112 +27,41 @@ form input — use `SegmentedControl` for the latter (it's a `radiogroup`; this
43
27
  |---|---|---|
44
28
  | `--sc-kit--tabs--height` | Tab height | per size (28 / 36 px) |
45
29
  | `--sc-kit--tabs--font-size` | Tab label font size | per size |
30
+ | `--sc-kit--tabs--font-weight` | Tab label font weight | per size (sm: regular / md: medium) |
46
31
  | `--sc-kit--tabs--padding-inline` | Per-tab horizontal padding | per size (10 / 14 px) |
47
32
  | `--sc-kit--tabs--radius` | Tab radius (pills) / tray radius | `--sc-kit--radius--sm` / `--sc-kit--radius--md` |
48
33
  | `--sc-kit--tabs--color` | Default tab text color | `--sc-kit--color--text--secondary` |
49
34
  | `--sc-kit--tabs--color--hover` | Hover text color | `--sc-kit--color--text--primary` |
50
- | `--sc-kit--tabs--color--active` | Active tab text color | default: `--sc-kit--color--accent`, pills: `--sc-kit--color--text--primary` |
35
+ | `--sc-kit--tabs--color--active` | Active tab text color | default: `--sc-kit--color--accent`, pills/document: `--sc-kit--color--text--primary` |
51
36
  | `--sc-kit--tabs--color--disabled` | Disabled tab text color | `--sc-kit--color--text--muted` |
52
37
  | `--sc-kit--tabs--accent` | Underline bar color (default variant) | `--sc-kit--color--accent` |
53
38
  | `--sc-kit--tabs--border-color` | Bottom border color (default variant) | `--sc-kit--color--border` |
54
39
  | `--sc-kit--tabs--pills--background` | Inset tray background (pills variant) | `--sc-kit--color--bg--active` |
55
40
  | `--sc-kit--tabs--pills--background--active` | Active tab background (pills variant) | `--sc-kit--color--bg--panel` |
56
41
  | `--sc-kit--tabs--pills--shadow--active` | Active tab lift shadow (pills variant) | `--sc-kit--shadow--sm` |
42
+ | `--sc-kit--tabs--document--background` | Inactive tab background (document variant) | `neutral-200` / `dark-500` |
43
+ | `--sc-kit--tabs--document--background--active` | Active tab background (document variant) — matches the canvas | `--sc-kit--color--bg--app` |
44
+ | `--sc-kit--tabs--document--background--hover` | Inactive tab hover background (document variant) | `--sc-kit--color--bg--hover` |
57
45
  -->
58
46
 
59
47
  <style>.tabs {
60
- --_tabs--height: var(--sc-kit--tabs--height, 2.25rem);
61
- --_tabs--font-size: var(--sc-kit--tabs--font-size, var(--sc-kit--font-size--md));
62
- --_tabs--padding-inline: var(--sc-kit--tabs--padding-inline, 0.875rem);
63
- --_tabs--radius: var(--sc-kit--tabs--radius, var(--sc-kit--radius--sm));
64
- --_tabs--color: var(--sc-kit--tabs--color, var(--sc-kit--color--text--secondary));
65
- --_tabs--color-hover: var(--sc-kit--tabs--color--hover, var(--sc-kit--color--text--primary));
66
- --_tabs--color-active: var(--sc-kit--tabs--color--active, var(--_tabs--color-active-default));
67
- --_tabs--color-disabled: var(--sc-kit--tabs--color--disabled, var(--sc-kit--color--text--muted));
68
- --_tabs--accent: var(--sc-kit--tabs--accent, var(--sc-kit--color--accent));
69
48
  --_tabs--border-color: var(--sc-kit--tabs--border-color, var(--sc-kit--color--border));
70
49
  --_tabs--pills-background: var(--sc-kit--tabs--pills--background, var(--sc-kit--color--bg--active));
71
- --_tabs--pills-background-active: var(--sc-kit--tabs--pills--background--active, var(--sc-kit--color--bg--panel));
72
- --_tabs--pills-shadow-active: var(--sc-kit--tabs--pills--shadow--active, var(--sc-kit--shadow--sm));
73
50
  display: inline-flex;
74
51
  align-items: center;
75
52
  gap: 0.25rem;
76
53
  font-family: inherit;
77
54
  }
78
- .tabs--sm {
79
- --sc-kit--tabs--height: 1.75rem;
80
- --sc-kit--tabs--font-size: var(--sc-kit--font-size--sm);
81
- --sc-kit--tabs--padding-inline: 0.625rem;
82
- }
83
- .tabs--md {
84
- --sc-kit--tabs--height: 2.25rem;
85
- --sc-kit--tabs--font-size: var(--sc-kit--font-size--md);
86
- --sc-kit--tabs--padding-inline: 0.875rem;
87
- }
88
55
  .tabs--default {
89
- --_tabs--color-active-default: var(--_tabs--accent);
90
56
  border-bottom: 1px solid var(--_tabs--border-color);
91
57
  }
92
- .tabs--default .tabs__tab {
93
- border-radius: 0;
94
- }
95
- .tabs--default .tabs__tab--active {
96
- color: var(--_tabs--color-active);
97
- }
98
- .tabs--default .tabs__tab--active::after {
99
- content: "";
100
- position: absolute;
101
- left: var(--_tabs--padding-inline);
102
- right: var(--_tabs--padding-inline);
103
- bottom: -1px;
104
- height: 2px;
105
- background: var(--_tabs--accent);
106
- border-radius: 2px;
107
- }
108
58
  .tabs--pills {
109
- --_tabs--color-active-default: var(--sc-kit--color--text--primary);
110
59
  gap: 0;
111
60
  padding: 0.25rem;
112
61
  background: var(--_tabs--pills-background);
113
62
  border-radius: var(--sc-kit--radius--md);
114
63
  }
115
- .tabs--pills .tabs__tab--active {
116
- color: var(--_tabs--color-active);
117
- background: var(--_tabs--pills-background-active);
118
- box-shadow: var(--_tabs--pills-shadow-active);
119
- }
120
- .tabs__tab {
121
- position: relative;
122
- display: inline-flex;
123
- align-items: center;
124
- justify-content: center;
125
- height: var(--_tabs--height);
126
- padding: 0 var(--_tabs--padding-inline);
127
- background: transparent;
128
- border: none;
129
- border-radius: var(--_tabs--radius);
130
- color: var(--_tabs--color);
131
- font-family: inherit;
132
- font-size: var(--_tabs--font-size);
133
- font-weight: var(--sc-kit--font-weight--medium);
134
- line-height: var(--sc-kit--leading--tight);
135
- cursor: pointer;
136
- white-space: nowrap;
137
- min-width: 0;
138
- transition: color var(--sc-kit--duration--base) var(--sc-kit--ease--default), background-color var(--sc-kit--duration--base) var(--sc-kit--ease--default);
139
- }
140
- .tabs__tab:where(:hover):not(.tabs__tab--disabled):not(.tabs__tab--active) {
141
- color: var(--_tabs--color-hover);
142
- }
143
- .tabs__tab--disabled {
144
- color: var(--_tabs--color-disabled);
145
- cursor: default;
146
- }
147
- .tabs__tab:focus-visible {
148
- outline: 2px solid var(--sc-kit--color--border--focus);
149
- outline-offset: 2px;
150
- }
151
- .tabs__label {
152
- display: inline-block;
153
- max-width: 100%;
64
+ .tabs--document {
65
+ gap: 0;
66
+ align-items: flex-end;
154
67
  }</style>
@@ -1,22 +1,19 @@
1
- export type Tab<TValue = unknown> = {
2
- value: TValue;
3
- label: string;
4
- /** Non-interactive — click does nothing. */
5
- disabled?: boolean;
6
- };
1
+ import type { Tab, TabsSize, TabsVariant } from './types';
7
2
  declare function $$render<T>(): {
8
3
  props: {
9
4
  value: T;
10
5
  /** `NoInfer<T>` ensures `T` narrows from `value` only; inline tab literals type-check against the value's T without widening — no `as const` needed at callsites. */
11
6
  tabs: Tab<NoInfer<T>>[];
12
7
  /** @default 'default' */
13
- variant?: "default" | "pills";
8
+ variant?: TabsVariant;
14
9
  /** @default 'md' */
15
- size?: "sm" | "md";
10
+ size?: TabsSize;
16
11
  /** Equality for object T values. @default reference equality (`===`) */
17
12
  compare?: (a: T, b: T) => boolean;
18
13
  on?: {
19
14
  change?: (value: T) => void;
15
+ /** Providing this renders a close affordance on each non-disabled tab (works in any variant). */
16
+ close?: (value: T) => void;
20
17
  };
21
18
  };
22
19
  exports: {};
@@ -40,11 +37,13 @@ interface $$IsomorphicComponent {
40
37
  }
41
38
  /**
42
39
  * Tabs — header strip for switching between content panels. Panel rendering is consumer-controlled
43
- * (switch on the active `value`). Two visual variants:
40
+ * (switch on the active `value`). Three visual variants:
44
41
  *
45
42
  * - **`default`**: underline — a 2px accent bar travels under the active tab; the strip has a bottom border.
46
43
  * - **`pills`**: filled — the active tab lifts as a card on an inset tray.
44
+ * - **`document`**: browser-style tabs — flush, rounded-top; the active tab background matches the canvas so it reads as continuous with the content below.
47
45
  *
46
+ * Providing `on.close` renders a close affordance on each non-disabled tab (works in any variant).
48
47
  * Each tab carries an optional `disabled` flag (non-interactive). For navigation, not single-choice
49
48
  * form input — use `SegmentedControl` for the latter (it's a `radiogroup`; this is a `tablist`).
50
49
  *
@@ -53,17 +52,21 @@ interface $$IsomorphicComponent {
53
52
  * |---|---|---|
54
53
  * | `--sc-kit--tabs--height` | Tab height | per size (28 / 36 px) |
55
54
  * | `--sc-kit--tabs--font-size` | Tab label font size | per size |
55
+ * | `--sc-kit--tabs--font-weight` | Tab label font weight | per size (sm: regular / md: medium) |
56
56
  * | `--sc-kit--tabs--padding-inline` | Per-tab horizontal padding | per size (10 / 14 px) |
57
57
  * | `--sc-kit--tabs--radius` | Tab radius (pills) / tray radius | `--sc-kit--radius--sm` / `--sc-kit--radius--md` |
58
58
  * | `--sc-kit--tabs--color` | Default tab text color | `--sc-kit--color--text--secondary` |
59
59
  * | `--sc-kit--tabs--color--hover` | Hover text color | `--sc-kit--color--text--primary` |
60
- * | `--sc-kit--tabs--color--active` | Active tab text color | default: `--sc-kit--color--accent`, pills: `--sc-kit--color--text--primary` |
60
+ * | `--sc-kit--tabs--color--active` | Active tab text color | default: `--sc-kit--color--accent`, pills/document: `--sc-kit--color--text--primary` |
61
61
  * | `--sc-kit--tabs--color--disabled` | Disabled tab text color | `--sc-kit--color--text--muted` |
62
62
  * | `--sc-kit--tabs--accent` | Underline bar color (default variant) | `--sc-kit--color--accent` |
63
63
  * | `--sc-kit--tabs--border-color` | Bottom border color (default variant) | `--sc-kit--color--border` |
64
64
  * | `--sc-kit--tabs--pills--background` | Inset tray background (pills variant) | `--sc-kit--color--bg--active` |
65
65
  * | `--sc-kit--tabs--pills--background--active` | Active tab background (pills variant) | `--sc-kit--color--bg--panel` |
66
66
  * | `--sc-kit--tabs--pills--shadow--active` | Active tab lift shadow (pills variant) | `--sc-kit--shadow--sm` |
67
+ * | `--sc-kit--tabs--document--background` | Inactive tab background (document variant) | `neutral-200` / `dark-500` |
68
+ * | `--sc-kit--tabs--document--background--active` | Active tab background (document variant) — matches the canvas | `--sc-kit--color--bg--app` |
69
+ * | `--sc-kit--tabs--document--background--hover` | Inactive tab hover background (document variant) | `--sc-kit--color--bg--hover` |
67
70
  */
68
71
  declare const Cmp: $$IsomorphicComponent;
69
72
  type Cmp<T> = InstanceType<typeof Cmp<T>>;
@@ -1,2 +1,2 @@
1
1
  export { default as Tabs } from './cmp.tabs.svelte';
2
- export type { Tab } from './cmp.tabs.svelte';
2
+ export type { Tab } from './types';
@@ -0,0 +1,169 @@
1
+ <script lang="ts" generics="T">import { IconSlot } from '../icon';
2
+ import { TabsLocalization } from './tabs-localization';
3
+ import IconDismiss from '@fluentui/svg-icons/icons/dismiss_20_regular.svg?raw';
4
+ const { tab, variant, size, active, on } = $props();
5
+ const localization = new TabsLocalization();
6
+ const closable = $derived(!!on?.close && !tab.disabled);
7
+ const handleClick = () => {
8
+ if (tab.disabled || active) {
9
+ return;
10
+ }
11
+ on?.select?.(tab.value);
12
+ };
13
+ const handleClose = (event) => {
14
+ event.stopPropagation();
15
+ on?.close?.(tab.value);
16
+ };
17
+ const handleCloseKeydown = (event) => {
18
+ if (event.key === 'Enter' || event.key === ' ') {
19
+ event.preventDefault();
20
+ event.stopPropagation();
21
+ on?.close?.(tab.value);
22
+ }
23
+ };
24
+ </script>
25
+
26
+ <button
27
+ type="button"
28
+ class="tab tab--{variant} tab--{size}"
29
+ class:tab--disabled={tab.disabled}
30
+ class:tab--hoverable={!active && !tab.disabled}
31
+ class:tab--default-active={variant === 'default' && active}
32
+ class:tab--pills-active={variant === 'pills' && active}
33
+ class:tab--document-active={variant === 'document' && active}
34
+ class:tab--document-hoverable={variant === 'document' && !active && !tab.disabled}
35
+ role="tab"
36
+ aria-selected={active}
37
+ disabled={tab.disabled}
38
+ onclick={handleClick}>
39
+ <span class="tab__label">{tab.label}</span>
40
+ {#if closable}
41
+ <span class="tab__close" role="button" tabindex="0" aria-label={localization.close} onclick={handleClose} onkeydown={handleCloseKeydown}>
42
+ <IconSlot icon={IconDismiss} size="sm" />
43
+ </span>
44
+ {/if}
45
+ </button>
46
+
47
+ <style>.tab {
48
+ --_tab--height: var(--sc-kit--tabs--height, 2.25rem);
49
+ --_tab--font-size: var(--sc-kit--tabs--font-size, var(--sc-kit--font-size--md));
50
+ --_tab--font-weight: var(--sc-kit--tabs--font-weight, var(--sc-kit--font-weight--medium));
51
+ --_tab--padding-inline: var(--sc-kit--tabs--padding-inline, 0.875rem);
52
+ --_tab--radius: var(--sc-kit--tabs--radius, var(--sc-kit--radius--sm));
53
+ --_tab--color: var(--sc-kit--tabs--color, var(--sc-kit--color--text--secondary));
54
+ --_tab--color-hover: var(--sc-kit--tabs--color--hover, var(--sc-kit--color--text--primary));
55
+ --_tab--color-active: var(--sc-kit--tabs--color--active, var(--_tab--color-active-default));
56
+ --_tab--color-disabled: var(--sc-kit--tabs--color--disabled, var(--sc-kit--color--text--muted));
57
+ --_tab--accent: var(--sc-kit--tabs--accent, var(--sc-kit--color--accent));
58
+ --_tab--pills-background-active: var(--sc-kit--tabs--pills--background--active, var(--sc-kit--color--bg--panel));
59
+ --_tab--pills-shadow-active: var(--sc-kit--tabs--pills--shadow--active, var(--sc-kit--shadow--sm));
60
+ --_tab--document-background: var(--sc-kit--tabs--document--background, light-dark(#e5e7eb, #272727));
61
+ --_tab--document-background-active: var(--sc-kit--tabs--document--background--active, var(--sc-kit--color--bg--app));
62
+ --_tab--document-background-hover: var(--sc-kit--tabs--document--background--hover, var(--sc-kit--color--bg--hover));
63
+ position: relative;
64
+ display: inline-flex;
65
+ align-items: center;
66
+ justify-content: center;
67
+ height: var(--_tab--height);
68
+ padding: 0 var(--_tab--padding-inline);
69
+ background: transparent;
70
+ border: none;
71
+ border-radius: var(--_tab--radius);
72
+ color: var(--_tab--color);
73
+ font-family: inherit;
74
+ font-size: var(--_tab--font-size);
75
+ font-weight: var(--_tab--font-weight);
76
+ line-height: var(--sc-kit--leading--tight);
77
+ cursor: pointer;
78
+ white-space: nowrap;
79
+ min-width: 0;
80
+ transition: color var(--sc-kit--duration--base) var(--sc-kit--ease--default), background-color var(--sc-kit--duration--base) var(--sc-kit--ease--default);
81
+ }
82
+ .tab--sm {
83
+ --sc-kit--tabs--height: 1.75rem;
84
+ --sc-kit--tabs--font-size: var(--sc-kit--font-size--sm);
85
+ --sc-kit--tabs--font-weight: var(--sc-kit--font-weight--regular);
86
+ --sc-kit--tabs--padding-inline: 0.625rem;
87
+ }
88
+ .tab--md {
89
+ --sc-kit--tabs--height: 2.25rem;
90
+ --sc-kit--tabs--font-size: var(--sc-kit--font-size--md);
91
+ --sc-kit--tabs--font-weight: var(--sc-kit--font-weight--medium);
92
+ --sc-kit--tabs--padding-inline: 0.875rem;
93
+ }
94
+ .tab--hoverable:hover {
95
+ color: var(--_tab--color-hover);
96
+ }
97
+ .tab--disabled {
98
+ color: var(--_tab--color-disabled);
99
+ cursor: default;
100
+ }
101
+ .tab--default {
102
+ --_tab--color-active-default: var(--_tab--accent);
103
+ border-radius: 0;
104
+ }
105
+ .tab--default-active {
106
+ color: var(--_tab--color-active);
107
+ }
108
+ .tab--default-active::after {
109
+ content: "";
110
+ position: absolute;
111
+ left: var(--_tab--padding-inline);
112
+ right: var(--_tab--padding-inline);
113
+ bottom: -1px;
114
+ height: 2px;
115
+ background: var(--_tab--accent);
116
+ border-radius: 2px;
117
+ }
118
+ .tab--pills {
119
+ --_tab--color-active-default: var(--sc-kit--color--text--primary);
120
+ }
121
+ .tab--pills-active {
122
+ color: var(--_tab--color-active);
123
+ background: var(--_tab--pills-background-active);
124
+ box-shadow: var(--_tab--pills-shadow-active);
125
+ }
126
+ .tab--document {
127
+ --_tab--color-active-default: var(--sc-kit--color--text--primary);
128
+ --_tab--height: 2rem;
129
+ --_tab--padding-inline: 0.75rem;
130
+ justify-content: flex-start;
131
+ gap: 0.5rem;
132
+ min-width: 7rem;
133
+ background: var(--_tab--document-background);
134
+ border-radius: 0.25rem 0.5rem 0 0;
135
+ }
136
+ .tab--document-hoverable:hover {
137
+ background: var(--_tab--document-background-hover);
138
+ }
139
+ .tab--document-active {
140
+ color: var(--_tab--color-active);
141
+ background: var(--_tab--document-background-active);
142
+ }
143
+ .tab:focus-visible {
144
+ outline: 2px solid var(--sc-kit--color--border--focus);
145
+ outline-offset: 2px;
146
+ }
147
+ .tab__label {
148
+ display: inline-block;
149
+ max-width: 100%;
150
+ }
151
+ .tab__close {
152
+ display: inline-flex;
153
+ align-items: center;
154
+ justify-content: center;
155
+ flex-shrink: 0;
156
+ margin-left: auto;
157
+ padding: 0.125rem;
158
+ color: var(--sc-kit--color--text--secondary);
159
+ border-radius: var(--sc-kit--radius--sm);
160
+ cursor: pointer;
161
+ transition: color var(--sc-kit--duration--base) var(--sc-kit--ease--default), background-color var(--sc-kit--duration--base) var(--sc-kit--ease--default);
162
+ }
163
+ .tab__close:hover {
164
+ color: var(--sc-kit--color--text--primary);
165
+ }
166
+ .tab__close:focus-visible {
167
+ outline: 2px solid var(--sc-kit--color--border--focus);
168
+ outline-offset: 2px;
169
+ }</style>
@@ -0,0 +1,34 @@
1
+ import type { Tab, TabsSize, TabsVariant } from './types';
2
+ declare function $$render<T>(): {
3
+ props: {
4
+ tab: Tab<T>;
5
+ variant: TabsVariant;
6
+ size: TabsSize;
7
+ active: boolean;
8
+ on?: {
9
+ select?: (value: T) => void;
10
+ close?: (value: T) => void;
11
+ };
12
+ };
13
+ exports: {};
14
+ bindings: "";
15
+ slots: {};
16
+ events: {};
17
+ };
18
+ declare class __sveltets_Render<T> {
19
+ props(): ReturnType<typeof $$render<T>>['props'];
20
+ events(): ReturnType<typeof $$render<T>>['events'];
21
+ slots(): ReturnType<typeof $$render<T>>['slots'];
22
+ bindings(): "";
23
+ exports(): {};
24
+ }
25
+ interface $$IsomorphicComponent {
26
+ new <T>(options: import('svelte').ComponentConstructorOptions<ReturnType<__sveltets_Render<T>['props']>>): import('svelte').SvelteComponent<ReturnType<__sveltets_Render<T>['props']>, ReturnType<__sveltets_Render<T>['events']>, ReturnType<__sveltets_Render<T>['slots']>> & {
27
+ $$bindings?: ReturnType<__sveltets_Render<T>['bindings']>;
28
+ } & ReturnType<__sveltets_Render<T>['exports']>;
29
+ <T>(internal: unknown, props: ReturnType<__sveltets_Render<T>['props']> & {}): ReturnType<__sveltets_Render<T>['exports']>;
30
+ z_$$bindings?: ReturnType<__sveltets_Render<any>['bindings']>;
31
+ }
32
+ declare const Tab: $$IsomorphicComponent;
33
+ type Tab<T> = InstanceType<typeof Tab<T>>;
34
+ export default Tab;
@@ -0,0 +1,3 @@
1
+ export declare class TabsLocalization {
2
+ get close(): string;
3
+ }
@@ -0,0 +1,12 @@
1
+ import { AppLocale } from '../../core/locale';
2
+ const loc = {
3
+ close: {
4
+ en: 'Close tab',
5
+ no: 'Lukk fane'
6
+ }
7
+ };
8
+ export class TabsLocalization {
9
+ get close() {
10
+ return loc.close[AppLocale.current];
11
+ }
12
+ }
@@ -0,0 +1,8 @@
1
+ export type TabsVariant = 'default' | 'pills' | 'document';
2
+ export type TabsSize = 'sm' | 'md';
3
+ export type Tab<TValue = unknown> = {
4
+ value: TValue;
5
+ label: string;
6
+ /** Non-interactive — click does nothing. */
7
+ disabled?: boolean;
8
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -80,14 +80,14 @@ switch left + label right, intrinsic width).
80
80
  --_row--label-color: var(--sc-kit--toggle-row--label--color, var(--sc-kit--color--text--primary));
81
81
  --_row--sub-font-size: var(--sc-kit--toggle-row--sub--font-size, var(--sc-kit--font-size--sm));
82
82
  --_row--sub-color: var(--sc-kit--toggle-row--sub--color, var(--sc-kit--color--text--muted));
83
- display: inline-flex;
83
+ display: flex;
84
84
  align-items: center;
85
85
  gap: var(--_row--gap);
86
+ width: fit-content;
86
87
  max-width: 100%;
87
88
  cursor: pointer;
88
89
  }
89
90
  .toggle-row--spread {
90
- display: flex;
91
91
  justify-content: space-between;
92
92
  width: 100%;
93
93
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@streamscloud/kit",
3
- "version": "0.9.0",
3
+ "version": "0.9.2",
4
4
  "author": "StreamsCloud",
5
5
  "repository": {
6
6
  "type": "git",
@@ -91,6 +91,10 @@
91
91
  "types": "./dist/core/validation/index.d.ts",
92
92
  "svelte": "./dist/core/validation/index.js"
93
93
  },
94
+ "./ui/ai-panel": {
95
+ "types": "./dist/ui/ai-panel/index.d.ts",
96
+ "svelte": "./dist/ui/ai-panel/index.js"
97
+ },
94
98
  "./ui/avatar": {
95
99
  "types": "./dist/ui/avatar/index.d.ts",
96
100
  "svelte": "./dist/ui/avatar/index.js"
@@ -465,7 +469,6 @@
465
469
  "yup": "^1.7.1"
466
470
  },
467
471
  "devDependencies": {
468
- "@eslint/compat": "^2.1.0",
469
472
  "@eslint/js": "^10.0.1",
470
473
  "@floating-ui/dom": "^1.7.6",
471
474
  "@fluentui/svg-icons": "^1.1.326",