@inglorious/web 2.0.1 → 2.1.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.
package/README.md CHANGED
@@ -164,6 +164,77 @@ mount(store, renderApp, document.getElementById("root"))
164
164
 
165
165
  The router automatically intercepts clicks on local `<a>` tags and handles browser back/forward events, keeping your UI in sync with the URL.
166
166
 
167
+ ### 3. Programmatic Navigation
168
+
169
+ To navigate from your JavaScript code, dispatch a `navigate` event.
170
+
171
+ ```javascript
172
+ api.notify("navigate", "/users/456")
173
+
174
+ // Or navigate back in history
175
+ api.notify("navigate", -1)
176
+ ```
177
+
178
+ ---
179
+
180
+ ## Table
181
+
182
+ `@inglorious/web` includes a `table` type for displaying data in a tabular format. It's designed to be flexible and customizable.
183
+
184
+ ### 1. Add the `table` type
185
+
186
+ To use it, import the `table` type and its CSS, then create an entity for your table. You must define the `data` to be displayed and can optionally provide `columns` definitions.
187
+
188
+ ```javascript
189
+ // In your entity definition file
190
+ import { table } from "@inglorious/web"
191
+
192
+ // Import base styles and a theme. You can create your own theme.
193
+ import "@inglorious/web/table/base.css"
194
+ import "@inglorious/web/table/theme.css"
195
+
196
+ export default {
197
+ ...table,
198
+ data: [
199
+ { id: 1, name: "Product A", price: 100 },
200
+ { id: 2, name: "Product B", price: 150 },
201
+ ],
202
+ columns: [
203
+ { id: "id", label: "ID" },
204
+ { id: "name", label: "Product Name" },
205
+ { id: "price", label: "Price" },
206
+ ],
207
+ }
208
+ ```
209
+
210
+ ### 2. Custom Rendering
211
+
212
+ You can customize how data is rendered in the table cells by overriding the `renderValue` method. This is useful for formatting values or displaying custom content.
213
+
214
+ The example below from `examples/apps/web-table/src/product-table/product-table.js` shows how to format values based on a `formatter` property in the column definition.
215
+
216
+ ```javascript
217
+ import { table } from "@inglorious/web"
218
+ import { format } from "date-fns"
219
+
220
+ const formatters = {
221
+ isAvailable: (val) => (val ? "✔️" : "❌"),
222
+ createdAt: (val) => format(val, "dd/MM/yyyy HH:mm"),
223
+ }
224
+
225
+ export const productTable = {
226
+ ...table,
227
+
228
+ renderValue(value, column) {
229
+ return formatters[column.formatter]?.(value) ?? value
230
+ },
231
+ }
232
+ ```
233
+
234
+ ### 3. Theming
235
+
236
+ The table comes with a base stylesheet (`@inglorious/web/table/base.css`) and a default theme (`@inglorious/web/table/theme.css`). You can create your own theme by creating a new CSS file and styling the table elements to match your application's design.
237
+
167
238
  ---
168
239
 
169
240
  ## Forms
@@ -315,17 +386,6 @@ const store = createStore({ types, entities })
315
386
 
316
387
  See `src/list.js` in the package for the implementation details and the `examples/apps/web-list` demo for a complete working example. In the demo the `productList` type extends the `list` type and provides `renderItem(item, index)` to render each visible item — see `examples/apps/web-list/src/product-list/product-list.js`.
317
388
 
318
- ### 3. Programmatic Navigation
319
-
320
- To navigate from your JavaScript code, dispatch a `navigate` event.
321
-
322
- ```javascript
323
- api.notify("navigate", "/users/456")
324
-
325
- // Or navigate back in history
326
- api.notify("navigate", -1)
327
- ```
328
-
329
389
  ## API Reference
330
390
 
331
391
  **`mount(store, renderFn, element)`**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inglorious/web",
3
- "version": "2.0.1",
3
+ "version": "2.1.1",
4
4
  "description": "A new web framework that leverages the power of the Inglorious Store combined with the performance and simplicity of lit-html.",
5
5
  "author": "IceOnFire <antony.mistretta@gmail.com> (https://ingloriouscoderz.it)",
6
6
  "license": "MIT",
@@ -25,7 +25,9 @@
25
25
  ".": {
26
26
  "types": "./types/index.d.ts",
27
27
  "import": "./src/index.js"
28
- }
28
+ },
29
+ "./table/base.css": "./src/table/base.css",
30
+ "./table/theme.css": "./src/table/theme.css"
29
31
  },
30
32
  "files": [
31
33
  "src",
@@ -36,13 +38,13 @@
36
38
  },
37
39
  "dependencies": {
38
40
  "lit-html": "^3.3.1",
39
- "@inglorious/store": "7.1.0",
41
+ "@inglorious/store": "7.1.1",
40
42
  "@inglorious/utils": "3.7.0"
41
43
  },
42
44
  "devDependencies": {
43
45
  "prettier": "^3.6.2",
44
46
  "vite": "^7.1.3",
45
- "@inglorious/eslint-config": "1.0.1"
47
+ "@inglorious/eslint-config": "1.1.0"
46
48
  },
47
49
  "engines": {
48
50
  "node": ">= 22"
package/src/index.js CHANGED
@@ -2,10 +2,13 @@ export { form, getFieldError, getFieldValue, isFieldTouched } from "./form.js"
2
2
  export { list } from "./list.js"
3
3
  export { mount } from "./mount.js"
4
4
  export { router } from "./router.js"
5
- export { getPaginationInfo, getRows, getSortDirection, table } from "./table.js"
5
+ export { table } from "./table.js"
6
6
  export { createStore } from "@inglorious/store"
7
7
  export { createDevtools } from "@inglorious/store/client/devtools.js"
8
8
  export { createSelector } from "@inglorious/store/select.js"
9
9
  export { html, render, svg } from "lit-html"
10
+ export { choose } from "lit-html/directives/choose.js"
10
11
  export { classMap } from "lit-html/directives/class-map.js"
12
+ export { ref } from "lit-html/directives/ref.js"
11
13
  export { repeat } from "lit-html/directives/repeat.js"
14
+ export { styleMap } from "lit-html/directives/style-map.js"
package/src/list.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { html } from "lit-html"
2
2
  import { ref } from "lit-html/directives/ref.js"
3
+ import { styleMap } from "lit-html/directives/style-map.js"
3
4
 
4
5
  const LIST_START = 0
5
6
  const PRETTY_INDEX = 1
@@ -37,11 +38,15 @@ export const list = {
37
38
  entity.visibleRange = { start, end }
38
39
  },
39
40
 
40
- measureHeight(entity, containerEl) {
41
+ mount(entity, containerEl) {
41
42
  const firstItem = containerEl.querySelector("[data-index]")
42
43
  if (!firstItem) return
43
44
 
44
45
  entity.itemHeight = firstItem.offsetHeight
46
+ entity.visibleRange = {
47
+ start: 0,
48
+ end: Math.ceil(entity.viewportHeight / entity.itemHeight),
49
+ }
45
50
  },
46
51
 
47
52
  render(entity, api) {
@@ -65,24 +70,33 @@ export const list = {
65
70
 
66
71
  return html`
67
72
  <div
68
- style="height: ${viewportHeight}px; overflow: auto"
73
+ style=${styleMap({ height: `${viewportHeight}px`, overflow: "auto" })}
69
74
  @scroll=${(e) => api.notify(`#${entity.id}:scroll`, e.target)}
70
75
  ${ref((el) => {
71
76
  if (el && !itemHeight) {
72
77
  queueMicrotask(() => {
73
- api.notify(`#${entity.id}:measureHeight`, el)
78
+ api.notify(`#${entity.id}:mount`, el)
74
79
  })
75
80
  }
76
81
  })}
77
82
  >
78
- <div style="height: ${totalHeight}px; position: relative">
83
+ <div
84
+ style=${styleMap({
85
+ height: `${totalHeight}px`,
86
+ position: "relative",
87
+ })}
88
+ >
79
89
  ${visibleItems.map((item, idx) => {
80
90
  const absoluteIndex = visibleRange.start + idx
81
91
  const top = absoluteIndex * height
82
92
 
83
93
  return html`
84
94
  <div
85
- style="position: absolute; top: ${top}px; width: 100%"
95
+ style=${styleMap({
96
+ position: "absolute",
97
+ top: `${top}px`,
98
+ width: "100%",
99
+ })}
86
100
  data-index=${absoluteIndex}
87
101
  >
88
102
  ${type.renderItem(item, absoluteIndex, api)}
@@ -0,0 +1,34 @@
1
+ .iw-table {
2
+ user-select: none;
3
+
4
+ .iw-table-header {
5
+ display: flex;
6
+ flex-direction: column;
7
+ }
8
+
9
+ .iw-table-header-row {
10
+ display: flex;
11
+ }
12
+
13
+ .iw-table-header-column {
14
+ display: flex;
15
+ flex-direction: column;
16
+ }
17
+
18
+ .iw-table-filter-input,
19
+ .iw-table-filter-select {
20
+ width: 100%;
21
+ }
22
+
23
+ .iw-table-searchbar {
24
+ width: 100%;
25
+ }
26
+
27
+ .iw-table-row {
28
+ display: flex;
29
+ }
30
+
31
+ .iw-table-footer-row {
32
+ display: flex;
33
+ }
34
+ }
@@ -0,0 +1,42 @@
1
+ import { html } from "@inglorious/web"
2
+
3
+ const INPUT_TYPE = {
4
+ date: "date",
5
+ time: "time",
6
+ datetime: "datetime-local",
7
+ }
8
+
9
+ export const dateFilter = {
10
+ render(entity, column, api) {
11
+ const filter = entity.filters[column.id] ?? {}
12
+
13
+ return html`<input
14
+ name=${`${column.id}Min`}
15
+ type=${INPUT_TYPE[column.filter.type]}
16
+ value=${entity.filters[column.id]}
17
+ @input=${(event) => {
18
+ const value = event.target.value
19
+ const formattedValue = value ? new Date(value).getTime() : null
20
+
21
+ api.notify(`#${entity.id}:filterChange`, {
22
+ columnId: column.id,
23
+ value: { ...filter, min: formattedValue },
24
+ })
25
+ }}
26
+ />
27
+ <input
28
+ name=${`${column.id}Max`}
29
+ type=${INPUT_TYPE[column.filter.type]}
30
+ value=${entity.filters[column.id]}
31
+ @input=${(event) => {
32
+ const value = event.target.value
33
+ const formattedValue = value ? new Date(value).getTime() : null
34
+
35
+ api.notify(`#${entity.id}:filterChange`, {
36
+ columnId: column.id,
37
+ value: { ...filter, max: formattedValue },
38
+ })
39
+ }}
40
+ />`
41
+ },
42
+ }
@@ -0,0 +1,22 @@
1
+ import { html } from "@inglorious/web"
2
+
3
+ export const numberFilter = {
4
+ render(entity, column, api) {
5
+ return html`<input
6
+ name=${column.id}
7
+ type="number"
8
+ placeholder=${column.filter.placeholder ?? "="}
9
+ value=${entity.filters[column.id]}
10
+ @input=${(event) => {
11
+ const value = event.target.value
12
+ const formattedValue = value ? Number(value) : null
13
+
14
+ api.notify(`#${entity.id}:filterChange`, {
15
+ columnId: column.id,
16
+ value: formattedValue,
17
+ })
18
+ }}
19
+ class="iw-table-cell-number"
20
+ />`
21
+ },
22
+ }
@@ -0,0 +1,40 @@
1
+ import { html } from "@inglorious/web"
2
+
3
+ export const rangeFilter = {
4
+ render(entity, column, api) {
5
+ const filter = entity.filters[column.id] ?? {}
6
+
7
+ return html`<input
8
+ name=${`${column.id}Min`}
9
+ type="number"
10
+ placeholder=${column.filter.placeholder ?? "≥"}
11
+ value=${filter.min}
12
+ @input=${(event) => {
13
+ const value = event.target.value
14
+ const formattedValue = value ? Number(value) : null
15
+
16
+ api.notify(`#${entity.id}:filterChange`, {
17
+ columnId: column.id,
18
+ value: { ...filter, min: formattedValue },
19
+ })
20
+ }}
21
+ class="iw-table-cell-number"
22
+ />
23
+ <input
24
+ name=${`${column.id}Max`}
25
+ type="number"
26
+ placeholder=${column.filter.placeholder ?? "≤"}
27
+ value=${filter.max}
28
+ @input=${(event) => {
29
+ const value = event.target.value
30
+ const formattedValue = value ? Number(value) : null
31
+
32
+ api.notify(`#${entity.id}:filterChange`, {
33
+ columnId: column.id,
34
+ value: { ...filter, max: formattedValue },
35
+ })
36
+ }}
37
+ class="iw-table-cell-number"
38
+ />`
39
+ },
40
+ }
@@ -0,0 +1,34 @@
1
+ import { html } from "@inglorious/web"
2
+
3
+ export const selectFilter = {
4
+ render(entity, column, api) {
5
+ return html`<select
6
+ name=${column.id}
7
+ ?multiple=${column.filter.isMultiple}
8
+ autocomplete="off"
9
+ value=${entity.filters[column.id]}
10
+ @change=${(event) => {
11
+ const value = event.target.value
12
+ const formattedValue = value ? format(value, column.type) : null
13
+
14
+ api.notify(`#${entity.id}:filterChange`, {
15
+ columnId: column.id,
16
+ value: formattedValue,
17
+ })
18
+ }}
19
+ >
20
+ ${column.filter.options.map(
21
+ (option) => html`<option value=${option}>${option}</option>`,
22
+ )}
23
+ </select>`
24
+ },
25
+ }
26
+
27
+ function format(value, type) {
28
+ if (type === "number") return Number(value)
29
+ if (type === "boolean")
30
+ return value === "true" ? true : value === "false" ? false : null
31
+ if (["date", "time", "datetime"].includes(type))
32
+ return new Date(value).getTime()
33
+ return value
34
+ }
@@ -0,0 +1,22 @@
1
+ import { html } from "@inglorious/web"
2
+
3
+ export const textFilter = {
4
+ render(entity, column, api) {
5
+ return html`<input
6
+ name=${column.id}
7
+ type="text"
8
+ placeholder=${column.filter.placeholder ?? "Contains..."}
9
+ autocomplete="off"
10
+ value=${entity.filters[column.id]}
11
+ @input=${(event) => {
12
+ const value = event.target.value
13
+ const formattedValue = value || null
14
+
15
+ api.notify(`#${entity.id}:filterChange`, {
16
+ columnId: column.id,
17
+ value: formattedValue,
18
+ })
19
+ }}
20
+ />`
21
+ },
22
+ }
@@ -0,0 +1,24 @@
1
+ import { choose, html } from "@inglorious/web"
2
+
3
+ import { dateFilter } from "./filters/date"
4
+ import { numberFilter } from "./filters/number"
5
+ import { rangeFilter } from "./filters/range"
6
+ import { selectFilter } from "./filters/select"
7
+ import { textFilter } from "./filters/text"
8
+
9
+ export const filters = {
10
+ render(entity, column, api) {
11
+ return html`${choose(
12
+ column.filter.type,
13
+ [
14
+ ["number", () => numberFilter.render(entity, column, api)],
15
+ ["range", () => rangeFilter.render(entity, column, api)],
16
+ ["select", () => selectFilter.render(entity, column, api)],
17
+ ["date", () => dateFilter.render(entity, column, api)],
18
+ ["time", () => dateFilter.render(entity, column, api)],
19
+ ["datetime", () => dateFilter.render(entity, column, api)],
20
+ ],
21
+ () => textFilter.render(entity, column, api),
22
+ )}`
23
+ },
24
+ }