@lavalogic/scoria 0.40.12 → 0.40.13

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.
@@ -9,10 +9,13 @@
9
9
 
10
10
  <script lang="ts">
11
11
  import { loadingText } from '../Helpers/Helpers.svelte.js';
12
+ import { usePrefersReducedMotion } from '../Helpers/prefersReducedMotion.svelte.js';
12
13
  import { Size } from '../Types/Internal/Size.js';
13
14
  import { Colour } from '../scss/colours.js';
14
- import Action from './Action.svelte';
15
+ import { cubicOut } from 'svelte/easing';
16
+ import { slide } from 'svelte/transition';
15
17
  import type { CollapsibleCardProps } from './CollapsibleCardProps.js';
18
+ import Icon from './Icon.svelte';
16
19
 
17
20
  let {
18
21
  title,
@@ -26,6 +29,9 @@
26
29
  const contentId = `collapsible-card-${generateWeakId()}`;
27
30
 
28
31
  const cardMaxHeight = $derived(maxHeight != null && maxHeight > 0 ? `${maxHeight}px` : undefined);
32
+
33
+ const reducedMotion = usePrefersReducedMotion();
34
+ const slideDuration = $derived(reducedMotion.matches ? 0 : 200);
29
35
  </script>
30
36
 
31
37
  <div
@@ -34,7 +40,16 @@
34
40
  class:is-collapsed={isCollapsed}
35
41
  class:grow
36
42
  >
37
- <div class="heading">
43
+ <button
44
+ type="button"
45
+ class="heading"
46
+ class:is-collapsed={isCollapsed}
47
+ aria-expanded={!isCollapsed}
48
+ aria-controls={contentId}
49
+ onclick={() => {
50
+ isCollapsed = !isCollapsed;
51
+ }}
52
+ >
38
53
  <div class="left">
39
54
  <h3>{title}</h3>
40
55
  {#if subtitle}
@@ -45,34 +60,26 @@
45
60
  {/await}
46
61
  {/if}
47
62
  </div>
48
- <div class="right">
49
- <Action
50
- label={isCollapsed ? 'Expand' : 'Collapse'}
51
- aria-expanded={!isCollapsed}
52
- aria-controls={contentId}
53
- onclick={() => {
54
- isCollapsed = !isCollapsed;
55
- }}
56
- size={Size.TripleXLarge}
57
- icon={{
58
- name: isCollapsed ? 'circle-plus' : 'circle-minus',
59
- colour: Colour['$ui-blue-5'],
60
- size: Size.Medium,
61
- }}
63
+ <span class="indicator">
64
+ <Icon
65
+ name={isCollapsed ? 'circle-plus' : 'circle-minus'}
66
+ colour={Colour['$ui-blue-5']}
67
+ size={Size.Medium}
62
68
  />
63
- </div>
64
- </div>
69
+ </span>
70
+ </button>
65
71
 
66
- <div
67
- id={contentId}
68
- class="content"
69
- class:is-collapsed={isCollapsed}
70
- aria-hidden={isCollapsed}
71
- >
72
- {#if children}
73
- {@render children()}
74
- {/if}
75
- </div>
72
+ {#if !isCollapsed}
73
+ <div
74
+ id={contentId}
75
+ class="content"
76
+ transition:slide={{ duration: slideDuration, easing: cubicOut }}
77
+ >
78
+ {#if children}
79
+ {@render children()}
80
+ {/if}
81
+ </div>
82
+ {/if}
76
83
  </div>
77
84
 
78
85
  <style>.collapsible-card {
@@ -89,35 +96,43 @@
89
96
  .collapsible-card.grow {
90
97
  flex: 1;
91
98
  }
92
- .collapsible-card .heading {
99
+
100
+ .heading {
93
101
  display: flex;
94
102
  flex-flow: row nowrap;
95
- transition: margin ease-out 0.15s 0s;
103
+ align-items: center;
104
+ width: 100%;
105
+ font: inherit;
106
+ text-align: start;
107
+ cursor: pointer;
108
+ background: transparent;
109
+ border: none;
96
110
  box-sizing: border-box;
97
111
  padding: 0 0.5rem 0.5rem 0.5rem;
98
112
  }
99
- .collapsible-card .heading .left {
113
+ .heading .left {
100
114
  display: flex;
101
115
  flex-flow: column nowrap;
102
116
  justify-content: center;
117
+ gap: 0.25rem;
103
118
  }
104
- .collapsible-card .heading .left h3 {
119
+ .heading .left h3 {
105
120
  margin: 0;
106
121
  margin-top: 0.25rem;
107
122
  color: #46525b;
108
123
  font-weight: 500;
109
124
  font-size: 1.125rem;
110
125
  }
111
- .collapsible-card .heading .left span {
112
- margin-top: auto;
113
- margin-bottom: 0.25rem;
126
+ .heading .left span {
114
127
  color: #647d8e;
115
128
  font-size: 0.875rem;
116
129
  }
117
- .collapsible-card .heading .right {
130
+ .heading .indicator {
118
131
  margin-left: auto;
132
+ display: inline-flex;
133
+ align-items: center;
119
134
  }
120
- .collapsible-card:not(.is-collapsed) .heading {
135
+ .heading:not(.is-collapsed) {
121
136
  margin-bottom: 1rem;
122
137
  border-bottom: solid 1px #dae0e5;
123
138
  }
@@ -125,49 +140,9 @@
125
140
  div.content {
126
141
  display: flex;
127
142
  flex-flow: column nowrap;
128
- }
129
- @media (prefers-reduced-motion: no-preference) {
130
- div.content {
131
- transition: max-height ease-out 0.15s 0s, opacity ease-out 0.15s 0.1s;
132
- }
133
- }
134
- div.content {
135
143
  max-height: var(--card-max-height, 100%);
136
144
  height: max-content;
137
145
  min-height: 0;
138
- opacity: 1;
139
146
  width: 100%;
140
- }
141
- @media (prefers-reduced-motion: no-preference) {
142
- div.content.is-collapsed {
143
- animation: accordion ease-out 0.15s 0s forwards;
144
- }
145
- }
146
- div.content.is-collapsed {
147
- max-height: 0px;
148
- opacity: 0;
149
- overflow: hidden;
150
- }
151
- @media (prefers-reduced-motion: no-preference) {
152
- @keyframes accordion {
153
- 0% {
154
- opacity: 1;
155
- max-height: var(--card-max-height, 90vh);
156
- overflow: auto;
157
- }
158
- 100% {
159
- opacity: 0;
160
- max-height: 0px;
161
- overflow: hidden;
162
- }
163
- }
164
- }
165
-
166
- @media (prefers-reduced-motion: reduce) {
167
- .heading,
168
- div.content,
169
- div.content.is-collapsed {
170
- animation-duration: 0.001ms !important;
171
- transition-duration: 0.001ms !important;
172
- }
147
+ overflow: auto;
173
148
  }</style>
@@ -0,0 +1,93 @@
1
+ <svelte:options runes />
2
+
3
+ <!--
4
+ @component
5
+ A labelled, collapsible group for a side menu: a header row (label + chevron)
6
+ that expands and collapses the items supplied as the default snippet. Pair
7
+ several of these in a menu column to build a grouped navigation tree.
8
+ -->
9
+ <script lang="ts">
10
+ import { usePrefersReducedMotion } from '../Helpers/prefersReducedMotion.svelte.js';
11
+ import { Colour } from '../scss/colours.js';
12
+ import { Size } from '../Types/Internal/Size.js';
13
+ import { cubicOut } from 'svelte/easing';
14
+ import { slide } from 'svelte/transition';
15
+ import type { CollapsibleMenuGroupProps } from './CollapsibleMenuGroupProps.js';
16
+ import Icon from './Icon.svelte';
17
+
18
+ let { title, isCollapsed = $bindable(false), children }: CollapsibleMenuGroupProps = $props();
19
+
20
+ const reducedMotion = usePrefersReducedMotion();
21
+ const slideDuration = $derived(reducedMotion.matches ? 0 : 200);
22
+ </script>
23
+
24
+ <div class="menu-group">
25
+ <button
26
+ type="button"
27
+ class="group-header"
28
+ aria-expanded={!isCollapsed}
29
+ onclick={() => {
30
+ isCollapsed = !isCollapsed;
31
+ }}
32
+ >
33
+ <span class="group-title">{title}</span>
34
+ <span
35
+ class="chevron"
36
+ class:collapsed={isCollapsed}
37
+ >
38
+ <Icon
39
+ name="chevron-down"
40
+ size={Size.Small}
41
+ colour={Colour['$ui-blue-11']}
42
+ />
43
+ </span>
44
+ </button>
45
+
46
+ {#if !isCollapsed}
47
+ <div
48
+ class="group-items"
49
+ transition:slide={{ duration: slideDuration, easing: cubicOut }}
50
+ >
51
+ {@render children()}
52
+ </div>
53
+ {/if}
54
+ </div>
55
+
56
+ <style>.menu-group {
57
+ border: solid 1px #cbd4da;
58
+ border-radius: 4px;
59
+ background-color: #ffffff;
60
+ overflow: hidden;
61
+ }
62
+
63
+ .group-header {
64
+ display: flex;
65
+ flex-flow: row nowrap;
66
+ align-items: center;
67
+ width: 100%;
68
+ gap: 0.5rem;
69
+ padding: 0.75rem 1.25rem;
70
+ font: inherit;
71
+ font-weight: 500;
72
+ color: #46525b;
73
+ text-align: start;
74
+ background: transparent;
75
+ border: none;
76
+ cursor: pointer;
77
+ }
78
+ .group-header .group-title {
79
+ flex-grow: 1;
80
+ }
81
+ .group-header .chevron {
82
+ display: inline-flex;
83
+ transition: transform 0.15s ease-out;
84
+ }
85
+ .group-header .chevron.collapsed {
86
+ transform: rotate(-90deg);
87
+ }
88
+
89
+ .group-items {
90
+ display: flex;
91
+ flex-flow: column nowrap;
92
+ border-top: solid 1px #cbd4da;
93
+ }</style>
@@ -0,0 +1,9 @@
1
+ import type { CollapsibleMenuGroupProps } from './CollapsibleMenuGroupProps.js';
2
+ /**
3
+ * A labelled, collapsible group for a side menu: a header row (label + chevron)
4
+ * that expands and collapses the items supplied as the default snippet. Pair
5
+ * several of these in a menu column to build a grouped navigation tree.
6
+ */
7
+ declare const CollapsibleMenuGroup: import("svelte").Component<CollapsibleMenuGroupProps, {}, "isCollapsed">;
8
+ type CollapsibleMenuGroup = ReturnType<typeof CollapsibleMenuGroup>;
9
+ export default CollapsibleMenuGroup;
@@ -0,0 +1,14 @@
1
+ import type { Snippet } from 'svelte';
2
+ /**
3
+ * Props for {@link CollapsibleMenuGroup} — a labelled group in a side menu
4
+ * whose items collapse and expand under a clickable header. The items are
5
+ * supplied as the default snippet.
6
+ */
7
+ export interface CollapsibleMenuGroupProps {
8
+ /** Header label. */
9
+ title: string;
10
+ /** Two-way bindable collapsed state. Defaults to expanded. */
11
+ isCollapsed?: boolean;
12
+ /** The menu items revealed when expanded. */
13
+ children: Snippet;
14
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -19,25 +19,41 @@ native radio group (the radio inputs are visually hidden but focusable).
19
19
  import type { OptionCardsProps } from './OptionCardsProps.js';
20
20
  import Icon from './Icon.svelte';
21
21
 
22
- let { name, options, selected, onchange }: OptionCardsProps<T> = $props();
22
+ let {
23
+ name,
24
+ options,
25
+ selected,
26
+ onchange,
27
+ multiple = false,
28
+ selectedValues,
29
+ ontoggle,
30
+ }: OptionCardsProps<T> = $props();
23
31
  </script>
24
32
 
25
33
  <div
26
34
  class="option-cards"
27
- role="radiogroup"
35
+ role={multiple ? 'group' : 'radiogroup'}
28
36
  >
29
37
  {#each options as option (option.value)}
30
- {@const isSelected = selected === option.value}
38
+ {@const isSelected = multiple
39
+ ? (selectedValues?.includes(option.value) ?? false)
40
+ : selected === option.value}
31
41
  <label
32
42
  class="card"
33
43
  class:selected={isSelected}
34
44
  >
35
45
  <input
36
46
  class="native"
37
- type="radio"
47
+ type={multiple ? 'checkbox' : 'radio'}
38
48
  {name}
39
49
  checked={isSelected}
40
- onchange={() => void onchange?.(option.value)}
50
+ onchange={() => {
51
+ if (multiple) {
52
+ ontoggle?.(option.value, !isSelected);
53
+ } else {
54
+ void onchange?.(option.value);
55
+ }
56
+ }}
41
57
  />
42
58
  {#if option.icon}
43
59
  <Icon
@@ -12,8 +12,17 @@ export interface OptionCardsProps<T> {
12
12
  name?: string;
13
13
  /** The option set rendered as cards. */
14
14
  options: ReadonlyArray<OptionCard<T>>;
15
- /** Currently-selected value. */
15
+ /** Currently-selected value (single-select mode). */
16
16
  selected?: T;
17
- /** Called when a card is chosen. May be `void` or `Promise<void>`. */
17
+ /** Called when a card is chosen (single-select mode). May be `void` or `Promise<void>`. */
18
18
  onchange?: (value: T) => void | Promise<void>;
19
+ /**
20
+ * Enable multi-select: cards become independent checkboxes. Use with
21
+ * `selectedValues` and `ontoggle` instead of `selected` / `onchange`.
22
+ */
23
+ multiple?: boolean;
24
+ /** Currently-selected values (multi-select mode). */
25
+ selectedValues?: ReadonlyArray<T>;
26
+ /** Called when a card is toggled on or off (multi-select mode). */
27
+ ontoggle?: (value: T, selected: boolean) => void;
19
28
  }
@@ -0,0 +1,85 @@
1
+ <svelte:options runes />
2
+
3
+ <!--
4
+ @component
5
+ A titled card laid out as a fixed-width menu column beside a main content
6
+ area. Both regions are supplied by the consumer as snippets (`menu` and the
7
+ default `children`), so the component only owns the framing and layout.
8
+ -->
9
+ <script lang="ts">
10
+ import type { SidebarPanelProps } from './SidebarPanelProps.js';
11
+
12
+ let { title, subtitle, menu, children, menuWidth = '14rem' }: SidebarPanelProps = $props();
13
+ </script>
14
+
15
+ <section class="sidebar-panel">
16
+ <header class="heading">
17
+ <h2>{title}</h2>
18
+ {#if subtitle}
19
+ <span class="subtitle">{subtitle}</span>
20
+ {/if}
21
+ </header>
22
+
23
+ <div class="body">
24
+ <aside
25
+ class="menu"
26
+ style="--menu-width: {menuWidth};"
27
+ >
28
+ {@render menu()}
29
+ </aside>
30
+ <div class="main">
31
+ {@render children()}
32
+ </div>
33
+ </div>
34
+ </section>
35
+
36
+ <style>.sidebar-panel {
37
+ display: flex;
38
+ flex-flow: column nowrap;
39
+ background-color: #ffffff;
40
+ border-radius: 4px;
41
+ box-shadow: 0px 0px 5px 0px rgba(0, 0, 0, 0.1333333333);
42
+ }
43
+
44
+ .heading {
45
+ display: flex;
46
+ flex-flow: column nowrap;
47
+ gap: 0.125rem;
48
+ padding: 1.5rem;
49
+ border-bottom: solid 1px #dae0e5;
50
+ }
51
+ .heading h2 {
52
+ margin: 0;
53
+ color: #46525b;
54
+ font-weight: 500;
55
+ font-size: 1.25rem;
56
+ }
57
+ .heading .subtitle {
58
+ color: #647d8e;
59
+ font-size: 0.875rem;
60
+ }
61
+
62
+ .body {
63
+ display: flex;
64
+ flex-flow: row nowrap;
65
+ align-items: stretch;
66
+ }
67
+
68
+ .menu {
69
+ width: var(--menu-width, 14rem);
70
+ flex-shrink: 0;
71
+ display: flex;
72
+ flex-flow: column nowrap;
73
+ gap: 0.75rem;
74
+ padding: 1.5rem;
75
+ border-right: solid 1px #dae0e5;
76
+ }
77
+
78
+ .main {
79
+ flex: 1;
80
+ min-width: 0;
81
+ display: flex;
82
+ flex-flow: column nowrap;
83
+ gap: 1.5rem;
84
+ padding: 1.5rem;
85
+ }</style>
@@ -0,0 +1,9 @@
1
+ import type { SidebarPanelProps } from './SidebarPanelProps.js';
2
+ /**
3
+ * A titled card laid out as a fixed-width menu column beside a main content
4
+ * area. Both regions are supplied by the consumer as snippets (`menu` and the
5
+ * default `children`), so the component only owns the framing and layout.
6
+ */
7
+ declare const SidebarPanel: import("svelte").Component<SidebarPanelProps, {}, "">;
8
+ type SidebarPanel = ReturnType<typeof SidebarPanel>;
9
+ export default SidebarPanel;
@@ -0,0 +1,18 @@
1
+ import type { Snippet } from 'svelte';
2
+ /**
3
+ * Props for {@link SidebarPanel} — a titled card with a fixed-width menu
4
+ * column on the left and a main content area on the right. Generic layout
5
+ * shell; the consumer supplies both regions as snippets.
6
+ */
7
+ export interface SidebarPanelProps {
8
+ /** Heading shown at the top of the panel. */
9
+ title: string;
10
+ /** Optional sub-heading shown under the title. */
11
+ subtitle?: string;
12
+ /** Left-hand menu column. */
13
+ menu: Snippet;
14
+ /** Main content area. */
15
+ children: Snippet;
16
+ /** Width of the menu column. Defaults to `'14rem'`. */
17
+ menuWidth?: string;
18
+ }
@@ -0,0 +1 @@
1
+ export {};
package/dist/index.d.ts CHANGED
@@ -22,6 +22,10 @@ export { default as Checkbox } from './Components/Checkbox.svelte';
22
22
  export type { CheckboxProps } from './Components/CheckboxProps.js';
23
23
  export { default as CollapsibleCard } from './Components/CollapsibleCard.svelte';
24
24
  export type { CollapsibleCardProps } from './Components/CollapsibleCardProps.js';
25
+ export { default as CollapsibleMenuGroup } from './Components/CollapsibleMenuGroup.svelte';
26
+ export type { CollapsibleMenuGroupProps } from './Components/CollapsibleMenuGroupProps.js';
27
+ export { default as SidebarPanel } from './Components/SidebarPanel.svelte';
28
+ export type { SidebarPanelProps } from './Components/SidebarPanelProps.js';
25
29
  export { default as ConditionBuilder } from './Components/ConditionBuilder/ConditionBuilder.svelte';
26
30
  export type { ConditionBuilderProps } from './Components/ConditionBuilder/ConditionBuilderProps.js';
27
31
  export { makeGroup, makeLeaf, type Combinator, type ConditionGroup, type ConditionLeaf, type ConditionNode, } from './Components/ConditionBuilder/ConditionTree.js';
package/dist/index.js CHANGED
@@ -14,6 +14,8 @@ export {} from './Components/BubbleProps.js';
14
14
  export { default as Button } from './Components/Button.svelte';
15
15
  export { default as Checkbox } from './Components/Checkbox.svelte';
16
16
  export { default as CollapsibleCard } from './Components/CollapsibleCard.svelte';
17
+ export { default as CollapsibleMenuGroup } from './Components/CollapsibleMenuGroup.svelte';
18
+ export { default as SidebarPanel } from './Components/SidebarPanel.svelte';
17
19
  export { default as ConditionBuilder } from './Components/ConditionBuilder/ConditionBuilder.svelte';
18
20
  export { makeGroup, makeLeaf, } from './Components/ConditionBuilder/ConditionTree.js';
19
21
  export {} from './Components/Contexts/ContextMenuOptionProps.js';
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@lavalogic/scoria",
3
3
  "description": "Svelte components used for the FloWMS Web Frontend",
4
- "version": "0.40.12",
4
+ "version": "0.40.13",
5
5
  "publishConfig": {
6
6
  "access": "restricted"
7
7
  },