@furystack/shades-common-components 3.5.3 → 3.6.1

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 (85) hide show
  1. package/dist/components/app-bar-link.js +8 -10
  2. package/dist/components/app-bar-link.js.map +1 -1
  3. package/dist/components/app-bar.js +3 -7
  4. package/dist/components/app-bar.js.map +1 -1
  5. package/dist/components/button.js +37 -27
  6. package/dist/components/button.js.map +1 -1
  7. package/dist/components/command-palette/command-palette-suggestion-list.js +1 -1
  8. package/dist/components/command-palette/command-palette-suggestion-list.js.map +1 -1
  9. package/dist/components/data-grid/body.js +3 -4
  10. package/dist/components/data-grid/body.js.map +1 -1
  11. package/dist/components/data-grid/data-grid-row.js +1 -1
  12. package/dist/components/data-grid/data-grid-row.js.map +1 -1
  13. package/dist/components/data-grid/data-grid.js +4 -15
  14. package/dist/components/data-grid/data-grid.js.map +1 -1
  15. package/dist/components/data-grid/footer.js +3 -10
  16. package/dist/components/data-grid/footer.js.map +1 -1
  17. package/dist/components/grid.js +1 -10
  18. package/dist/components/grid.js.map +1 -1
  19. package/dist/components/inputs/input.js +10 -14
  20. package/dist/components/inputs/input.js.map +1 -1
  21. package/dist/components/inputs/text-area.js +3 -7
  22. package/dist/components/inputs/text-area.js.map +1 -1
  23. package/dist/components/loader.js +0 -10
  24. package/dist/components/loader.js.map +1 -1
  25. package/dist/components/noty-list.js +3 -3
  26. package/dist/components/noty-list.js.map +1 -1
  27. package/dist/components/paper.js +2 -12
  28. package/dist/components/paper.js.map +1 -1
  29. package/dist/components/tabs.js +2 -10
  30. package/dist/components/tabs.js.map +1 -1
  31. package/dist/services/collection-service.js +16 -8
  32. package/dist/services/collection-service.js.map +1 -1
  33. package/dist/services/default-variable-theme.js +80 -0
  34. package/dist/services/default-variable-theme.js.map +1 -0
  35. package/dist/services/index.js +1 -0
  36. package/dist/services/index.js.map +1 -1
  37. package/dist/services/theme-provider-service.js +9 -7
  38. package/dist/services/theme-provider-service.js.map +1 -1
  39. package/package.json +7 -7
  40. package/src/components/app-bar-link.tsx +9 -12
  41. package/src/components/app-bar.tsx +3 -6
  42. package/src/components/button.tsx +44 -31
  43. package/src/components/command-palette/command-palette-suggestion-list.tsx +1 -1
  44. package/src/components/data-grid/body.tsx +6 -3
  45. package/src/components/data-grid/data-grid-row.tsx +6 -1
  46. package/src/components/data-grid/data-grid.tsx +16 -14
  47. package/src/components/data-grid/footer.tsx +3 -11
  48. package/src/components/grid.tsx +1 -10
  49. package/src/components/inputs/input.tsx +11 -16
  50. package/src/components/inputs/text-area.tsx +3 -9
  51. package/src/components/loader.tsx +0 -9
  52. package/src/components/noty-list.tsx +3 -3
  53. package/src/components/paper.tsx +3 -11
  54. package/src/components/tabs.tsx +2 -10
  55. package/src/services/collection-service.ts +54 -11
  56. package/src/services/default-variable-theme.ts +83 -0
  57. package/src/services/index.ts +1 -0
  58. package/src/services/theme-provider-service.ts +11 -8
  59. package/tsconfig.tsbuildinfo +1 -1
  60. package/types/components/app-bar-link.d.ts.map +1 -1
  61. package/types/components/app-bar.d.ts.map +1 -1
  62. package/types/components/button.d.ts.map +1 -1
  63. package/types/components/data-grid/body.d.ts +3 -1
  64. package/types/components/data-grid/body.d.ts.map +1 -1
  65. package/types/components/data-grid/data-grid-row.d.ts +1 -0
  66. package/types/components/data-grid/data-grid-row.d.ts.map +1 -1
  67. package/types/components/data-grid/data-grid.d.ts +8 -0
  68. package/types/components/data-grid/data-grid.d.ts.map +1 -1
  69. package/types/components/data-grid/footer.d.ts.map +1 -1
  70. package/types/components/grid.d.ts.map +1 -1
  71. package/types/components/inputs/input.d.ts +1 -2
  72. package/types/components/inputs/input.d.ts.map +1 -1
  73. package/types/components/inputs/text-area.d.ts +0 -2
  74. package/types/components/inputs/text-area.d.ts.map +1 -1
  75. package/types/components/loader.d.ts.map +1 -1
  76. package/types/components/paper.d.ts.map +1 -1
  77. package/types/components/tabs.d.ts.map +1 -1
  78. package/types/services/collection-service.d.ts +32 -2
  79. package/types/services/collection-service.d.ts.map +1 -1
  80. package/types/services/default-variable-theme.d.ts +7 -0
  81. package/types/services/default-variable-theme.d.ts.map +1 -0
  82. package/types/services/index.d.ts +1 -0
  83. package/types/services/index.d.ts.map +1 -1
  84. package/types/services/theme-provider-service.d.ts +2 -3
  85. package/types/services/theme-provider-service.d.ts.map +1 -1
@@ -8,7 +8,7 @@ import { DataGridRow } from './data-grid-row'
8
8
  export interface DataGridBodyProps<T> {
9
9
  service: CollectionService<T>
10
10
  onRowClick?: (row: T, ev: MouseEvent) => void
11
- onRowDoubleClick?: (entry: T) => void
11
+ onRowDoubleClick?: (entry: T, ev: MouseEvent) => void
12
12
  columns: Array<keyof T>
13
13
  rowComponents?: DataRowCells<T>
14
14
  style?: Partial<CSSStyleDeclaration>
@@ -16,6 +16,8 @@ export interface DataGridBodyProps<T> {
16
16
  unfocusedRowStyle?: Partial<CSSStyleDeclaration>
17
17
  selectedRowStyle?: Partial<CSSStyleDeclaration>
18
18
  unselectedRowStyle?: Partial<CSSStyleDeclaration>
19
+ emptyComponent?: JSX.Element
20
+ loaderComponent?: JSX.Element
19
21
  }
20
22
 
21
23
  export interface DataGridBodyState<T> {
@@ -44,13 +46,13 @@ export const DataGridBody: <T>(props: DataGridBodyProps<T>, children: ChildrenLi
44
46
  return (
45
47
  <div style={{ display: 'flex', height: '100%', justifyContent: 'center', alignItems: 'center', width: '100%' }}>
46
48
  {/* TODO: Skeleton */}
47
- <Loader style={{ height: '128px', width: '128px' }} />
49
+ {props.loaderComponent || <Loader style={{ height: '128px', width: '128px' }} />}
48
50
  </div>
49
51
  )
50
52
  }
51
53
 
52
54
  if (!state.data?.length) {
53
- return <div> - No Data - </div>
55
+ return props.emptyComponent || <div> - No Data - </div>
54
56
  }
55
57
 
56
58
  return (
@@ -62,6 +64,7 @@ export const DataGridBody: <T>(props: DataGridBodyProps<T>, children: ChildrenLi
62
64
  service={props.service}
63
65
  rowComponents={props.rowComponents}
64
66
  onRowClick={props.onRowClick}
67
+ onRowDoubleClick={props.onRowDoubleClick}
65
68
  focusedRowStyle={props.focusedRowStyle}
66
69
  unfocusedRowStyle={props.unfocusedRowStyle}
67
70
  selectedRowStyle={props.selectedRowStyle}
@@ -9,6 +9,7 @@ export interface DataGridRowProps<T> {
9
9
  service: CollectionService<T>
10
10
  rowComponents?: DataRowCells<T>
11
11
  onRowClick?: (row: T, event: MouseEvent) => void
12
+ onRowDoubleClick?: (row: T, event: MouseEvent) => void
12
13
  focusedRowStyle?: Partial<CSSStyleDeclaration>
13
14
  selectedRowStyle?: Partial<CSSStyleDeclaration>
14
15
  unfocusedRowStyle?: Partial<CSSStyleDeclaration>
@@ -100,7 +101,11 @@ export const DataGridRow: <T>(props: DataGridRowProps<T>, children: ChildrenList
100
101
  return (
101
102
  <>
102
103
  {columns.map((column) => (
103
- <td style={{ padding: '0.5em' }} onclick={(ev) => props.onRowClick?.(entry, ev)}>
104
+ <td
105
+ style={{ padding: '0.5em' }}
106
+ onclick={(ev) => props.onRowClick?.(entry, ev)}
107
+ ondblclick={(ev) => props.onRowDoubleClick?.(entry, ev)}
108
+ >
104
109
  {rowComponents?.[column]?.(entry, state) || rowComponents?.default?.(entry, state) || (
105
110
  <span>{entry[column]}</span>
106
111
  )}
@@ -56,23 +56,24 @@ export interface DataGridProps<T> {
56
56
  * Optional style to attach to grid rows when the row is not selected
57
57
  */
58
58
  unselectedRowStyle?: Partial<CSSStyleDeclaration>
59
+
60
+ /**
61
+ * An optional component to show if there are no rows to display
62
+ */
63
+ emptyComponent?: JSX.Element
64
+
65
+ /**
66
+ * An optional component to show while the data is loading
67
+ */
68
+ loaderComponent?: JSX.Element
59
69
  }
60
70
 
61
71
  export const DataGrid: <T>(props: DataGridProps<T>, children: ChildrenList) => JSX.Element<any, any> = Shade<
62
72
  DataGridProps<any>
63
73
  >({
64
74
  shadowDomName: 'shade-data-grid',
65
- resources: ({ injector, element, props }) => {
66
- const tp = injector.getInstance(ThemeProviderService)
75
+ resources: ({ element, props }) => {
67
76
  return [
68
- tp.theme.subscribe((t) => {
69
- const headers = element.querySelectorAll('th')
70
- const { r, g, b } = tp.getRgbFromColorString(t.background.paper)
71
- headers.forEach((header) => {
72
- header.style.color = t.text.secondary
73
- header.style.backgroundColor = `rgba(${r}, ${g}, ${b}, 0.3)`
74
- })
75
- }),
76
77
  new ClickAwayService(element, () => {
77
78
  props.service.hasFocus.setValue(false)
78
79
  }),
@@ -85,12 +86,10 @@ export const DataGrid: <T>(props: DataGridProps<T>, children: ChildrenList) => J
85
86
  },
86
87
  render: ({ props, injector }) => {
87
88
  const tp = injector.getInstance(ThemeProviderService)
88
- const theme = tp.theme.getValue()
89
+ const { theme } = tp
89
90
 
90
- const { r, g, b } = tp.getRgbFromColorString(theme.background.paper)
91
91
  const headerStyle: Partial<CSSStyleDeclaration> = {
92
- backgroundColor: `rgba(${r}, ${g}, ${b}, 0.3)`,
93
- backdropFilter: 'blur(10px)',
92
+ backdropFilter: 'blur(12px)',
94
93
  padding: '12px 0',
95
94
  color: theme.text.secondary,
96
95
  alignItems: 'center',
@@ -137,11 +136,14 @@ export const DataGrid: <T>(props: DataGridProps<T>, children: ChildrenList) => J
137
136
  service={props.service}
138
137
  rowComponents={props.rowComponents}
139
138
  onRowClick={(entry, ev) => props.service.handleRowClick(entry, ev)}
139
+ onRowDoubleClick={(entry) => props.service.handleRowDoubleClick(entry)}
140
140
  style={props.styles?.cell}
141
141
  focusedRowStyle={props.focusedRowStyle}
142
142
  selectedRowStyle={props.selectedRowStyle}
143
143
  unfocusedRowStyle={props.unfocusedRowStyle}
144
144
  unselectedRowStyle={props.unselectedRowStyle}
145
+ emptyComponent={props.emptyComponent}
146
+ loaderComponent={props.loaderComponent}
145
147
  />
146
148
  </table>
147
149
  <DataGridFooter service={props.service} />
@@ -9,16 +9,8 @@ export const DataGridFooter = Shade<{ service: CollectionService<any> }, { data:
9
9
  getInitialState: ({ props }) => ({
10
10
  data: props.service.data.getValue(),
11
11
  }),
12
- constructed: ({ props, updateState, injector, element }) => {
13
- const disposables = [
14
- props.service.data.subscribe((data) => updateState({ data })),
15
-
16
- injector.getInstance(ThemeProviderService).theme.subscribe((t) => {
17
- const el = element.querySelector('div') as HTMLDivElement
18
- el.style.color = t.text.secondary
19
- el.style.background = t.background.paper
20
- }),
21
- ]
12
+ constructed: ({ props, updateState }) => {
13
+ const disposables = [props.service.data.subscribe((data) => updateState({ data }))]
22
14
 
23
15
  return () => disposables.forEach((d) => d.dispose())
24
16
  },
@@ -27,7 +19,7 @@ export const DataGridFooter = Shade<{ service: CollectionService<any> }, { data:
27
19
  const currentQuerySettings = props.service.querySettings.getValue()
28
20
  const currentPage = Math.ceil(currentQuerySettings.skip || 0) / (currentQuerySettings.top || 1)
29
21
  const currentEntriesPerPage = currentQuerySettings.top || Infinity
30
- const theme = injector.getInstance(ThemeProviderService).theme.getValue()
22
+ const { theme } = injector.getInstance(ThemeProviderService)
31
23
 
32
24
  return (
33
25
  <div
@@ -25,17 +25,8 @@ export type RowCells<T> = {
25
25
 
26
26
  export const Grid: <T>(props: GridProps<T>, children: ChildrenList) => JSX.Element<any, any> = Shade({
27
27
  shadowDomName: 'shade-grid',
28
- resources: ({ injector, element }) => [
29
- injector.getInstance(ThemeProviderService).theme.subscribe((t) => {
30
- const headers = element.querySelectorAll('th')
31
- headers.forEach((header) => {
32
- header.style.color = t.text.secondary
33
- header.style.background = t.background.paper
34
- })
35
- }),
36
- ],
37
28
  render: ({ props, injector }) => {
38
- const theme = injector.getInstance(ThemeProviderService).theme.getValue()
29
+ const { theme } = injector.getInstance(ThemeProviderService)
39
30
  const headerStyle: Partial<CSSStyleDeclaration> = {
40
31
  padding: '0 0.51em',
41
32
  backgroundColor: theme.background.paper,
@@ -1,7 +1,7 @@
1
1
  import type { PartialElement } from '@furystack/shades'
2
2
  import { Shade, createComponent, attachStyles } from '@furystack/shades'
3
3
  import { ThemeProviderService } from '../..'
4
- import type { Palette, Theme } from '../../services'
4
+ import type { Palette } from '../../services'
5
5
 
6
6
  export interface TextInputProps extends PartialElement<HTMLInputElement> {
7
7
  /**
@@ -47,7 +47,6 @@ export interface TextInputProps extends PartialElement<HTMLInputElement> {
47
47
  }
48
48
 
49
49
  export type TextInputState = {
50
- theme: Theme
51
50
  value: string
52
51
  focused: boolean
53
52
  validity: ValidityState
@@ -69,12 +68,12 @@ const getLabelStyle = ({
69
68
  justifyContent: 'space-between',
70
69
  fontSize: '10px',
71
70
  color: props.disabled
72
- ? state.theme.text.disabled
71
+ ? themeProvider.theme.text.disabled
73
72
  : state.validity?.valid === false
74
- ? state.theme.palette.error.main
73
+ ? themeProvider.theme.palette.error.main
75
74
  : state.focused
76
- ? state.theme.text.primary
77
- : state.theme.text.secondary,
75
+ ? themeProvider.theme.text.primary
76
+ : themeProvider.theme.text.secondary,
78
77
  marginBottom: '1em',
79
78
  padding: '1em',
80
79
  borderRadius: '5px',
@@ -83,8 +82,8 @@ const getLabelStyle = ({
83
82
  ? themeProvider
84
83
  .getRgbFromColorString(
85
84
  state.validity?.valid === false
86
- ? state.theme.palette.error.main
87
- : state.theme.palette[props.defaultColor || 'primary'].main,
85
+ ? themeProvider.theme.palette.error.main
86
+ : themeProvider.theme.palette[props.defaultColor || 'primary'].main,
88
87
  )
89
88
  .update('a', state.focused ? 0.1 : 0.2)
90
89
  .toString()
@@ -93,10 +92,10 @@ const getLabelStyle = ({
93
92
  props.variant === 'outlined' || props.variant === 'contained'
94
93
  ? `0 0 0 1px ${
95
94
  state.validity?.valid === false
96
- ? state.theme.palette.error.main
95
+ ? themeProvider.theme.palette.error.main
97
96
  : state.focused
98
- ? state.theme.palette[props.defaultColor || 'primary'].main
99
- : state.theme.text.primary
97
+ ? themeProvider.theme.palette[props.defaultColor || 'primary'].main
98
+ : themeProvider.theme.text.primary
100
99
  }`
101
100
  : 'none',
102
101
  filter: props.disabled ? 'grayscale(100%)' : 'none',
@@ -109,15 +108,11 @@ const getLabelStyle = ({
109
108
 
110
109
  export const Input = Shade<TextInputProps, TextInputState>({
111
110
  shadowDomName: 'shade-input',
112
- getInitialState: ({ injector, props }) => ({
113
- theme: injector.getInstance(ThemeProviderService).theme.getValue(),
111
+ getInitialState: ({ props }) => ({
114
112
  value: props.value || '',
115
113
  focused: props.autofocus || false,
116
114
  validity: { valid: true } as ValidityState,
117
115
  }),
118
- resources: ({ injector, updateState }) => [
119
- injector.getInstance(ThemeProviderService).theme.subscribe((theme) => updateState({ theme })),
120
- ],
121
116
  compareState: ({ newState, element, props, injector }) => {
122
117
  const themeProvider = injector.getInstance(ThemeProviderService)
123
118
  const label = element.querySelector('label') as HTMLLabelElement
@@ -1,6 +1,5 @@
1
1
  import type { PartialElement } from '@furystack/shades'
2
2
  import { createComponent, Shade } from '@furystack/shades'
3
- import type { Theme } from '../../services'
4
3
  import { ThemeProviderService } from '../../services'
5
4
  import { promisifyAnimation } from '../../utils'
6
5
 
@@ -12,23 +11,18 @@ export interface TextAreaProps extends PartialElement<HTMLTextAreaElement> {
12
11
  }
13
12
 
14
13
  export type TextAreaInputState = {
15
- theme: Theme
16
14
  value?: string
17
15
  }
18
16
 
19
17
  export const TextArea = Shade<TextAreaProps, TextAreaInputState>({
20
18
  shadowDomName: 'shade-text-area',
21
- getInitialState: ({ injector, props }) => ({
22
- theme: injector.getInstance(ThemeProviderService).theme.getValue(),
19
+ getInitialState: ({ props }) => ({
23
20
  value: props.value,
24
21
  }),
25
- resources: ({ injector, updateState }) => {
26
- const themeProvider = injector.getInstance(ThemeProviderService)
27
- return [themeProvider.theme.subscribe((theme) => updateState({ theme }))]
28
- },
29
22
  render: ({ props, element, injector, getState }) => {
30
23
  const themeProvider = injector.getInstance(ThemeProviderService)
31
- const { theme, value } = getState()
24
+ const { theme } = themeProvider
25
+ const { value } = getState()
32
26
  const { palette } = theme
33
27
 
34
28
  return (
@@ -1,5 +1,4 @@
1
1
  import { Shade, createComponent } from '@furystack/shades'
2
- import { ThemeProviderService } from '../services'
3
2
  import { promisifyAnimation } from '../utils'
4
3
 
5
4
  interface LoaderProps {
@@ -15,14 +14,6 @@ interface LoaderProps {
15
14
 
16
15
  export const Loader = Shade<LoaderProps>({
17
16
  shadowDomName: 'shade-loader',
18
- resources: ({ injector, element }) => [
19
- injector.getInstance(ThemeProviderService).theme.subscribe((theme) => {
20
- const el = element.firstElementChild
21
- if (el) {
22
- ;(el as HTMLElement).style.borderBottom = `15px solid ${theme.palette.primary.main}`
23
- }
24
- }, true),
25
- ],
26
17
  render: ({ element, props }) => {
27
18
  element.style.display = 'inline-block'
28
19
  element.style.transformOrigin = 'center'
@@ -42,7 +42,7 @@ export const NotyComponent = Shade<{ model: NotyModel; onDismiss: () => void }>(
42
42
  },
43
43
  render: ({ props, injector, element }) => {
44
44
  const themeProvider = injector.getInstance(ThemeProviderService)
45
- const colors = themeProvider.theme.getValue().palette[props.model.type]
45
+ const colors = themeProvider.theme.palette[props.model.type]
46
46
  const headerTextColor = themeProvider.getTextColor(colors.dark)
47
47
  const textColor = themeProvider.getTextColor(colors.main)
48
48
 
@@ -51,8 +51,8 @@ export const NotyComponent = Shade<{ model: NotyModel; onDismiss: () => void }>(
51
51
  await promisifyAnimation(
52
52
  container,
53
53
  [
54
- { opacity: '1', height: `${container?.scrollHeight || 0}px` },
55
- { opacity: '0', height: '0px' },
54
+ { opacity: '1', height: `${container?.scrollHeight || 0}px`, margin: '8px 8px' },
55
+ { opacity: '0', height: '0px', margin: '0px 8px' },
56
56
  ],
57
57
  {
58
58
  fill: 'forwards',
@@ -4,15 +4,7 @@ import { ThemeProviderService } from '../services/theme-provider-service'
4
4
 
5
5
  export const Paper = Shade<PartialElement<HTMLDivElement> & { elevation?: 1 | 2 | 3 }>({
6
6
  shadowDomName: 'shade-paper',
7
- resources: ({ injector, element }) => {
8
- const themeProvider = injector.getInstance(ThemeProviderService)
9
- return [
10
- themeProvider.theme.subscribe((newTheme) => {
11
- ;(element.firstChild as HTMLDivElement).style.background = newTheme.background.paper
12
- ;(element.firstChild as HTMLDivElement).style.color = themeProvider.theme.getValue().text.secondary
13
- }),
14
- ]
15
- },
7
+
16
8
  render: ({ injector, props, children }) => {
17
9
  const themeProvider = injector.getInstance(ThemeProviderService)
18
10
  return (
@@ -21,8 +13,8 @@ export const Paper = Shade<PartialElement<HTMLDivElement> & { elevation?: 1 | 2
21
13
  style={{
22
14
  borderRadius: '3px',
23
15
  boxShadow: props.elevation ? `1px ${props.elevation}px ${props.elevation}px rgba(0,0,0,0.3)` : '',
24
- backgroundColor: themeProvider.theme.getValue().background.paper,
25
- color: themeProvider.theme.getValue().text.secondary,
16
+ backgroundColor: themeProvider.theme.background.paper,
17
+ color: themeProvider.theme.text.secondary,
26
18
  margin: '8px',
27
19
  padding: '6px 16px',
28
20
  ...props?.style,
@@ -19,7 +19,7 @@ export const Tabs = Shade<
19
19
  >({
20
20
  shadowDomName: 'shade-tabs',
21
21
  getInitialState: ({ props }) => ({ activeIndex: props.activeTab || 0 }),
22
- constructed: ({ injector, updateState, element }) => {
22
+ constructed: ({ injector, updateState }) => {
23
23
  const subscriptions = [
24
24
  injector.getInstance(LocationService).onLocationChanged.subscribe(() => {
25
25
  const { hash } = location
@@ -28,20 +28,12 @@ export const Tabs = Shade<
28
28
  page && updateState({ activeIndex: page })
29
29
  }
30
30
  }, true),
31
- injector.getInstance(ThemeProviderService).theme.subscribe((t) => {
32
- const headers = element.querySelectorAll('.shade-tabs-headers') as unknown as HTMLDivElement[]
33
- headers.forEach((header) => {
34
- const isActive = header.classList.contains('active')
35
- header.style.backgroundColor = isActive ? t.background.paper : t.background.default
36
- header.style.color = isActive ? t.text.primary : t.text.secondary
37
- })
38
- }),
39
31
  ]
40
32
  return () => subscriptions.forEach((s) => s.dispose())
41
33
  },
42
34
  render: ({ props, getState, updateState, injector }) => {
43
35
  const themeProvider = injector.getInstance(ThemeProviderService)
44
- const theme = themeProvider.theme.getValue()
36
+ const { theme } = themeProvider
45
37
  return (
46
38
  <div style={{ width: '100%', height: '100%', display: 'flex', flexDirection: 'column', ...props.containerStyle }}>
47
39
  <div
@@ -12,6 +12,40 @@ export type EntryLoader<T> = <TFields extends Array<keyof T>>(
12
12
  searchOptions: FindOptions<T, TFields>,
13
13
  ) => Promise<CollectionData<PartialResult<T, TFields>>>
14
14
 
15
+ export interface CollectionServiceOptions<T> {
16
+ /**
17
+ * A method used to retrieve the entries from the data source
18
+ */
19
+ loader: EntryLoader<T>
20
+ /**
21
+ * The default filter / top / skip / etc... options
22
+ */
23
+ defaultSettings: FindOptions<T, Array<keyof T>>
24
+ /**
25
+ * An optional field that can be used for quick search
26
+ */
27
+ searchField?: keyof T
28
+
29
+ /**
30
+ * @param entry The clicked entry
31
+ * optional callback for row clicks
32
+ */
33
+ onRowClick?: (entry: T) => void
34
+
35
+ /**
36
+ * Optional callback for row double clicks
37
+ *
38
+ * @param entry The clicked entry
39
+ */
40
+
41
+ onRowDoubleClick?: (entry: T) => void
42
+
43
+ /**
44
+ * An optional debounce interval in milliseconds
45
+ */
46
+ debounceMs?: number
47
+ }
48
+
15
49
  export class CollectionService<T> implements Disposable {
16
50
  public dispose() {
17
51
  this.querySettings.dispose()
@@ -102,10 +136,12 @@ export class CollectionService<T> implements Disposable {
102
136
  break
103
137
  }
104
138
  default:
105
- if (this.searchField && ev.key.length === 1) {
139
+ if (this.options.searchField && ev.key.length === 1) {
106
140
  const newSearchExpression = searchTerm + ev.key
107
141
  const newFocusedEntry = entries.find(
108
- (e) => this.searchField && (e[this.searchField] as any)?.toString().startsWith(newSearchExpression),
142
+ (e) =>
143
+ this.options.searchField &&
144
+ (e[this.options.searchField] as any)?.toString().startsWith(newSearchExpression),
109
145
  )
110
146
  this.focusedEntry.setValue(newFocusedEntry)
111
147
  this.searchTerm.setValue(newSearchExpression)
@@ -114,6 +150,7 @@ export class CollectionService<T> implements Disposable {
114
150
  }
115
151
 
116
152
  public handleRowClick(entry: T, ev: MouseEvent) {
153
+ this.options.onRowClick?.(entry)
117
154
  const currentSelectionValue = this.selection.getValue()
118
155
  const lastFocused = this.focusedEntry.getValue()
119
156
  if (ev.ctrlKey) {
@@ -141,18 +178,19 @@ export class CollectionService<T> implements Disposable {
141
178
  this.focusedEntry.setValue(entry)
142
179
  }
143
180
 
144
- constructor(
145
- fetch: EntryLoader<T>,
146
- defaultSettings: FindOptions<T, Array<keyof T>>,
147
- private readonly searchField?: keyof T,
148
- ) {
149
- this.querySettings = new ObservableValue<FindOptions<T, Array<keyof T>>>(defaultSettings)
150
- this.getEntries = debounce(async (options) => {
181
+ constructor(private options: CollectionServiceOptions<T>) {
182
+ this.querySettings = new ObservableValue<FindOptions<T, Array<keyof T>>>(this.options.defaultSettings)
183
+
184
+ const loader = this.options.debounceMs
185
+ ? debounce(this.options.loader, this.options.debounceMs)
186
+ : this.options.loader
187
+
188
+ this.getEntries = async (opt) => {
151
189
  await this.loadLock.acquire()
152
190
  try {
153
191
  this.error.setValue(undefined)
154
192
  this.isLoading.setValue(true)
155
- const result = await fetch(options)
193
+ const result = await loader(opt)
156
194
  this.data.setValue(result)
157
195
  return result
158
196
  } catch (error) {
@@ -162,7 +200,12 @@ export class CollectionService<T> implements Disposable {
162
200
  this.loadLock.release()
163
201
  this.isLoading.setValue(false)
164
202
  }
165
- }, 500)
203
+ }
204
+
166
205
  this.querySettings.subscribe((val) => this.getEntries(val), true)
167
206
  }
207
+
208
+ public async handleRowDoubleClick(entry: T) {
209
+ this.options.onRowDoubleClick?.(entry)
210
+ }
168
211
  }
@@ -0,0 +1,83 @@
1
+ import type { DeepPartial } from '@furystack/utils'
2
+ import type { Theme } from './theme-provider-service'
3
+ export const defaultVariableTheme: Theme = {
4
+ text: {
5
+ primary: 'var(--shades-theme-text-primary)',
6
+ secondary: 'var(--shades-theme-text-secondary)',
7
+ disabled: 'var(--shades-theme-text-disabled)',
8
+ },
9
+ button: {
10
+ active: 'var(--shades-theme-button-active)',
11
+ hover: 'var(--shades-theme-button-hover)',
12
+ selected: 'var(--shades-theme-button-selected)',
13
+ disabled: 'var(--shades-theme-button-disabled)',
14
+ disabledBackground: 'var(--shades-theme-button-disabled-background)',
15
+ },
16
+ background: {
17
+ default: 'var(--shades-theme-background-default)',
18
+ paper: 'var(--shades-theme-background-paper)',
19
+ },
20
+ palette: {
21
+ primary: {
22
+ light: 'var(--shades-theme-palette-primary-light)',
23
+ main: 'var(--shades-theme-palette-primary-main)',
24
+ dark: 'var(--shades-theme-palette-primary-dark)',
25
+ },
26
+ secondary: {
27
+ light: 'var(--shades-theme-palette-secondary-light)',
28
+ main: 'var(--shades-theme-palette-secondary-main)',
29
+ dark: 'var(--shades-theme-palette-secondary-dark)',
30
+ },
31
+ error: {
32
+ light: 'var(--shades-theme-palette-error-light)',
33
+ main: 'var(--shades-theme-palette-error-main)',
34
+ dark: 'var(--shades-theme-palette-error-dark)',
35
+ },
36
+ warning: {
37
+ light: 'var(--shades-theme-palette-warning-light)',
38
+ main: 'var(--shades-theme-palette-warning-main)',
39
+ dark: 'var(--shades-theme-palette-warning-dark)',
40
+ },
41
+ info: {
42
+ light: 'var(--shades-theme-palette-info-light)',
43
+ main: 'var(--shades-theme-palette-info-main)',
44
+ dark: 'var(--shades-theme-palette-info-dark)',
45
+ },
46
+ success: {
47
+ light: 'var(--shades-theme-palette-success-light)',
48
+ main: 'var(--shades-theme-palette-success-main)',
49
+ dark: 'var(--shades-theme-palette-success-dark)',
50
+ },
51
+ },
52
+ divider: 'var(--shades-theme-divider)',
53
+ }
54
+
55
+ export const setCssVariable = (key: string, value: string, root: HTMLElement) => {
56
+ root.style.setProperty(key.replace('var(', '').replace(')', ''), value)
57
+ }
58
+
59
+ export const getCssVariable = (key: string, root: HTMLElement = document.querySelector(':root') as HTMLElement) => {
60
+ return getComputedStyle(root).getPropertyValue(key.replace('var(', '').replace(')', ''))
61
+ }
62
+
63
+ const assignValue = <T extends object>(
64
+ target: T,
65
+ source: DeepPartial<T>,
66
+ root: HTMLElement,
67
+ assignFn = setCssVariable,
68
+ ) => {
69
+ const keys = Object.keys(target) as Array<keyof T>
70
+ keys.forEach((key) => {
71
+ if (typeof source[key] === 'object' && typeof target[key] === 'object') {
72
+ assignValue(target[key] as object, source[key] as object, root)
73
+ return
74
+ } else {
75
+ assignFn(target[key] as string, source[key] as string, root)
76
+ }
77
+ })
78
+ }
79
+
80
+ export const useThemeCssVariables = (theme: DeepPartial<Theme>) => {
81
+ const root = document.querySelector(':root') as HTMLElement
82
+ assignValue(defaultVariableTheme, theme, root)
83
+ }
@@ -5,3 +5,4 @@ export * from './default-dark-theme'
5
5
  export * from './default-light-theme'
6
6
  export * from './default-palette'
7
7
  export * from './noty-service'
8
+ export * from './default-variable-theme'
@@ -1,7 +1,6 @@
1
1
  import { Injectable } from '@furystack/inject'
2
2
  import type { DeepPartial } from '@furystack/utils'
3
- import { deepMerge, ObservableValue } from '@furystack/utils'
4
- import { defaultDarkTheme } from './default-dark-theme'
3
+ import { defaultVariableTheme, getCssVariable, useThemeCssVariables } from './default-variable-theme'
5
4
 
6
5
  export type Color = string // `#${string}` | `rgba(${number},${number},${number},${number})` |
7
6
 
@@ -66,7 +65,7 @@ export class RgbColor {
66
65
  @Injectable({ lifetime: 'singleton' })
67
66
  export class ThemeProviderService {
68
67
  /**
69
- *
68
+ * @deprecated does not respect CSS vars
70
69
  * @param color The background color
71
70
  * @param bright The Bright color
72
71
  * @param dark The Dark color
@@ -83,7 +82,11 @@ export class ThemeProviderService {
83
82
  * @param color The color string
84
83
  * @returns The parsed R,G,B, A values
85
84
  */
86
- public getRgbFromColorString(color: string) {
85
+ public getRgbFromColorString(color: string): RgbColor {
86
+ if (color.startsWith('var(--')) {
87
+ return this.getRgbFromColorString(getCssVariable(color))
88
+ }
89
+
87
90
  if (color.startsWith('#')) {
88
91
  if (color.length === 7) {
89
92
  const r = parseInt(color.substr(1, 2), 16)
@@ -111,16 +114,16 @@ export class ThemeProviderService {
111
114
  )
112
115
  }
113
116
  }
114
- throw Error(`Color format '${color} is not supported.'`)
117
+ throw Error(`Color format '${color}' is not supported.'`)
115
118
  }
116
119
 
117
- public readonly theme = new ObservableValue(defaultDarkTheme)
120
+ public readonly theme = defaultVariableTheme
118
121
 
119
122
  public set(change: DeepPartial<Theme>) {
120
- this.theme.setValue(deepMerge(this.theme.getValue(), change))
123
+ useThemeCssVariables(change)
121
124
  }
122
125
 
123
126
  public dispose() {
124
- this.theme.dispose()
127
+ /** */
125
128
  }
126
129
  }