@stacksjs/ui 0.70.58 → 0.70.60

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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@stacksjs/ui",
3
3
  "type": "module",
4
- "version": "0.70.58",
4
+ "version": "0.70.60",
5
5
  "description": "The Stacks UI engine.",
6
6
  "author": "Chris Breuer",
7
7
  "contributors": [
@@ -49,19 +49,21 @@
49
49
  "types": "dist/index.d.ts",
50
50
  "files": [
51
51
  "README.md",
52
- "dist",
53
- "src"
52
+ "dist"
54
53
  ],
55
54
  "scripts": {
56
55
  "build": "bun build.ts",
57
56
  "typecheck": "bun tsc --noEmit",
58
57
  "prepublishOnly": "bun run build"
59
58
  },
59
+ "dependencies": {
60
+ "@cwcss/crosswind": "^0.2.6"
61
+ },
60
62
  "devDependencies": {
61
63
  "@stacksjs/build": "workspace:*",
62
- "@stacksjs/config": "0.70.58",
64
+ "@stacksjs/config": "0.70.60",
63
65
  "better-dx": "^0.2.12",
64
- "@stacksjs/router": "0.70.58",
66
+ "@stacksjs/router": "0.70.60",
65
67
  "@stacksjs/stx": "^0.2.82"
66
68
  }
67
69
  }
File without changes
@@ -1,5 +0,0 @@
1
- export {
2
- Disclosure,
3
- DisclosureButton,
4
- DisclosurePanel,
5
- } from '@stacksjs/stx'
@@ -1 +0,0 @@
1
- export { Menu, MenuButton, MenuItem, MenuItems } from '@stacksjs/stx'
@@ -1,6 +0,0 @@
1
- export {
2
- Dialog,
3
- DialogDescription,
4
- DialogPanel,
5
- DialogTitle,
6
- } from '@stacksjs/stx'
@@ -1 +0,0 @@
1
- export { Popover, PopoverButton, PopoverPanel } from '@stacksjs/stx'
@@ -1,5 +0,0 @@
1
- export {
2
- RadioGroup,
3
- RadioGroupLabel,
4
- RadioGroupOption,
5
- } from '@stacksjs/stx'
@@ -1,6 +0,0 @@
1
- export {
2
- Combobox,
3
- ComboboxInput,
4
- ComboboxOption,
5
- ComboboxOptions,
6
- } from '@stacksjs/stx'
@@ -1 +0,0 @@
1
- export { Tab, TabGroup, TabList, TabPanel, TabPanels } from '@stacksjs/stx'
@@ -1 +0,0 @@
1
- export { Switch } from '@stacksjs/stx'
@@ -1 +0,0 @@
1
- export { TransitionChild, TransitionRoot } from '@stacksjs/stx'
package/src/components.ts DELETED
@@ -1,10 +0,0 @@
1
- export * from './components/autocomplete'
2
- export * from './components/disclosure'
3
- export * from './components/menu'
4
- export * from './components/modal'
5
- export * from './components/popover'
6
- export * from './components/radio-group'
7
- export * from './components/select'
8
- export * from './components/tabs'
9
- export * from './components/toggle'
10
- export * from './components/transition'
package/src/fonts.ts DELETED
@@ -1,205 +0,0 @@
1
- /**
2
- * Web-font preload + `@font-face` rendering helpers
3
- * (stacksjs/stacks#283 — prevent layout shifts).
4
- *
5
- * Layout shifts (the LS in CLS) happen when a page renders with the
6
- * fallback font, then re-flows once the web font loads. The two
7
- * mitigations this module ships against that:
8
- *
9
- * 1. **Preload** — emit `<link rel="preload" as="font" crossorigin>`
10
- * for each font listed in the UI config, so the browser starts the
11
- * font download in parallel with HTML parsing rather than waiting
12
- * to discover the URL via CSS.
13
- *
14
- * 2. **`font-display: swap`** — the default `@font-face` block uses
15
- * `font-display: swap`, which shows the fallback font immediately
16
- * and swaps in the web font once available. Layout still shifts
17
- * slightly at swap time, but the page is readable from t=0 instead
18
- * of being blocked by the font fetch.
19
- *
20
- * Apps drop a list of fonts into `config/ui.ts` and call these helpers
21
- * from their layout's `<head>` — see {@link renderFontHead}. Both
22
- * outputs are safe to inline into HTML (no user-controlled values
23
- * interpolate; the FontEntry shape is config-only).
24
- */
25
-
26
- /** One configured web font. */
27
- export interface FontEntry {
28
- /**
29
- * Font family name. Used as the `font-family` value in the emitted
30
- * `@font-face` block and referenced from your CSS.
31
- */
32
- family: string
33
- /**
34
- * Absolute or app-relative URL to the font file. Local files under
35
- * `public/` are preferred for the preload path since they share the
36
- * connection with the document; CDN-hosted fonts work but lose the
37
- * preload benefit when crossing origins without `crossorigin`.
38
- */
39
- src: string
40
- /**
41
- * Font file format passed to the `format(...)` clause. Defaults to
42
- * `'woff2'` because every shipping browser supports it; only set
43
- * this when the asset is actually a different format.
44
- *
45
- * @default 'woff2'
46
- */
47
- format?: 'woff2' | 'woff' | 'truetype' | 'opentype'
48
- /**
49
- * `font-weight` to apply on the `@font-face` block. Accepts CSS
50
- * keywords (`'normal'` / `'bold'`) or numeric weights (100-900).
51
- *
52
- * @default 'normal'
53
- */
54
- weight?: number | string
55
- /**
56
- * `font-style` on the `@font-face` block.
57
- *
58
- * @default 'normal'
59
- */
60
- style?: 'normal' | 'italic' | 'oblique'
61
- /**
62
- * `font-display` strategy. `'swap'` is the right default for the CLS
63
- * goal — see https://web.dev/font-display. `'optional'` is stricter
64
- * (skip the swap entirely on slow networks) and useful for highly
65
- * brand-sensitive surfaces; `'block'` re-introduces FOIT and should
66
- * be avoided unless you have a very specific reason.
67
- *
68
- * @default 'swap'
69
- */
70
- display?: 'auto' | 'swap' | 'block' | 'fallback' | 'optional'
71
- /**
72
- * Whether to emit a `<link rel="preload" as="font">` tag. Defaults
73
- * to `true` for the first 2 fonts (the typical "above-the-fold"
74
- * count before TCP slot exhaustion hurts more than it helps) — set
75
- * `false` on long lists' tail entries.
76
- */
77
- preload?: boolean
78
- /**
79
- * Optional `unicode-range`. Useful for subsetted fonts (e.g. Latin
80
- * vs CJK). Omitted from the `@font-face` block when undefined.
81
- *
82
- * @example
83
- * 'U+0000-00FF, U+0131, U+0152-0153'
84
- */
85
- unicodeRange?: string
86
- }
87
-
88
- /** Map `format` shorthand to the MIME-style label CSS expects. */
89
- const FORMAT_LABEL: Record<NonNullable<FontEntry['format']>, string> = {
90
- woff2: 'woff2',
91
- woff: 'woff',
92
- truetype: 'truetype',
93
- opentype: 'opentype',
94
- }
95
-
96
- /** Map `format` shorthand to the `<link>` MIME type. */
97
- const FORMAT_MIME: Record<NonNullable<FontEntry['format']>, string> = {
98
- woff2: 'font/woff2',
99
- woff: 'font/woff',
100
- truetype: 'font/ttf',
101
- opentype: 'font/otf',
102
- }
103
-
104
- /**
105
- * Render `<link rel="preload" as="font" crossorigin>` tags for every
106
- * font with `preload !== false`. Cross-origin attribute is always
107
- * emitted because most CDN-hosted fonts require it and same-origin
108
- * fonts ignore it harmlessly.
109
- *
110
- * Output is a single string with one tag per line, ready to drop into
111
- * a stx layout's `<head>` via `{{ renderFontPreloads(fonts) }}`.
112
- */
113
- export function renderFontPreloads(fonts: FontEntry[] = []): string {
114
- return fonts
115
- .filter(font => font.preload !== false)
116
- .map((font) => {
117
- const fmt = font.format ?? 'woff2'
118
- const type = FORMAT_MIME[fmt]
119
- return `<link rel="preload" href="${escapeAttr(font.src)}" as="font" type="${type}" crossorigin>`
120
- })
121
- .join('\n')
122
- }
123
-
124
- /**
125
- * Render an `@font-face { ... }` block for every entry. `font-display:
126
- * swap` is the default; override per-entry with `display`.
127
- *
128
- * Wrap the result in `<style>` tags when inlining into a layout's
129
- * `<head>`, or write to a separate CSS file and `<link>` it.
130
- */
131
- export function renderFontFaceCss(fonts: FontEntry[] = []): string {
132
- return fonts
133
- .map((font) => {
134
- const fmt = font.format ?? 'woff2'
135
- const fmtLabel = FORMAT_LABEL[fmt]
136
- const weight = font.weight ?? 'normal'
137
- const style = font.style ?? 'normal'
138
- const display = font.display ?? 'swap'
139
- const ur = font.unicodeRange
140
- return `@font-face {
141
- font-family: '${escapeFamily(font.family)}';
142
- src: url('${escapeUrl(font.src)}') format('${fmtLabel}');
143
- font-weight: ${weight};
144
- font-style: ${style};
145
- font-display: ${display};${ur ? `\n unicode-range: ${ur};` : ''}
146
- }`
147
- })
148
- .join('\n\n')
149
- }
150
-
151
- /**
152
- * Convenience helper: render BOTH the preload tags and the wrapped
153
- * `<style>...@font-face...</style>` block in one go, separated by a
154
- * newline. Drop this into the very top of your layout's `<head>` for
155
- * the maximal CLS win.
156
- *
157
- * @example
158
- * ```stx
159
- * <head>
160
- * {{ renderFontHead(config.ui.fonts) }}
161
- * <title>...</title>
162
- * </head>
163
- * ```
164
- */
165
- export function renderFontHead(fonts: FontEntry[] = []): string {
166
- if (fonts.length === 0) return ''
167
- const preloads = renderFontPreloads(fonts)
168
- const faces = renderFontFaceCss(fonts)
169
- return `${preloads}\n<style>\n${faces}\n</style>`
170
- }
171
-
172
- /**
173
- * Escape an HTML attribute value — `&` `<` `>` `"` `'`. The font config
174
- * is app-controlled, not user-controlled, so injection isn't a realistic
175
- * threat here; the escape is defense-in-depth for callers that pull
176
- * font URLs from a CMS or similar untrusted source.
177
- */
178
- function escapeAttr(s: string): string {
179
- return String(s).replace(/[&<>"']/g, (c) => {
180
- return ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', '\'': '&#39;' } as Record<string, string>)[c]!
181
- })
182
- }
183
-
184
- /**
185
- * Strip CSS-meaningful chars from a font URL inside `url('...')`.
186
- *
187
- * Removes anything that could break out of the `url('...')` wrapper:
188
- * quotes, parentheses, semicolons, braces (which would terminate the
189
- * `src:` declaration or `@font-face` block), comment delimiters
190
- * (`/*`), and whitespace control chars. `/`, `:`, `?`, `=`, `&`, `.`
191
- * are retained since they're legitimate URL chars.
192
- */
193
- function escapeUrl(s: string): string {
194
- return String(s).replace(/['"`()<>{};\\\r\n\t*]/g, '')
195
- }
196
-
197
- /**
198
- * Strip CSS-meaningful chars from a font-family identifier. Stricter
199
- * than {@link escapeUrl} because family names are short identifiers,
200
- * not URLs — only alphanumerics, spaces, dashes, dots, and underscores
201
- * survive.
202
- */
203
- function escapeFamily(s: string): string {
204
- return String(s).replace(/[^A-Z0-9 \-_.]/gi, '')
205
- }
package/src/index.ts DELETED
@@ -1,25 +0,0 @@
1
- import { ui } from '@stacksjs/config'
2
- import * as CssEngine from '@cwcss/crosswind'
3
-
4
- export { CssEngine, ui }
5
-
6
- // Web-font preload + @font-face helpers — used by stx layouts to
7
- // eliminate the FOUT/CLS that hits when a page renders with the
8
- // fallback font and then re-flows once the web font lands
9
- // (stacksjs/stacks#283).
10
- export {
11
- renderFontFaceCss,
12
- renderFontHead,
13
- renderFontPreloads,
14
- } from './fonts'
15
- export type { FontEntry } from './fonts'
16
-
17
- // Pagination view helpers — pure functions consumed by the
18
- // <Pagination> stx component (defaults/resources/components/Pagination.stx).
19
- // stacksjs/stacks#1909 P5.
20
- export {
21
- buildPageSequence,
22
- paginatorVariant,
23
- urlForPage,
24
- } from './pagination'
25
- export type { PaginatorView } from './pagination'
package/src/pagination.ts DELETED
@@ -1,115 +0,0 @@
1
- /**
2
- * Pagination view helpers (stacksjs/stacks#1909, P5 from #1910).
3
- *
4
- * Pure functions consumed by the `<Pagination>` stx component
5
- * (`defaults/resources/components/Pagination.stx`). Extracted here so
6
- * the page-sequence + URL-templating logic is unit-testable independent
7
- * of the stx render pipeline, and so apps that want to roll their own
8
- * pagination UI can reuse the same algorithms.
9
- *
10
- * The functions operate on the canonical paginator shapes from
11
- * `@stacksjs/orm` (Paginator / SimplePaginator / CursorPaginator), but
12
- * accept any duck-typed object with the right fields so callers don't
13
- * need a runtime import dependency on the orm module just to format
14
- * page numbers.
15
- */
16
-
17
- /**
18
- * Subset of {@link Paginator} fields that the view-side helpers actually
19
- * touch. Keeping this minimal keeps the helper decoupled from the orm.
20
- */
21
- export interface PaginatorView {
22
- current_page?: number
23
- last_page?: number
24
- prev_page_url?: string | null
25
- next_page_url?: string | null
26
- first_page_url?: string
27
- last_page_url?: string
28
- }
29
-
30
- /**
31
- * Build the page-number sequence for a full paginator, inserting an
32
- * ellipsis placeholder (`'…'`) for the gap between page 1 / the
33
- * current-page window / the last page.
34
- *
35
- * Examples (window=2):
36
- *
37
- * current=5, last=12 → [1, '…', 3, 4, 5, 6, 7, '…', 12]
38
- * current=1, last=3 → [1, 2, 3] (window covers all)
39
- * current=1, last=1 → [] (single page → no UI)
40
- * current=5, last=5 → [1, 2, 3, 4, 5] (last is current, no trailing ellipsis)
41
- *
42
- * Always anchors the sequence with `1` and `last_page` (when they
43
- * exist and differ from the current window) so users always have a
44
- * "jump to start" / "jump to end" affordance.
45
- *
46
- * @param current 1-indexed current page
47
- * @param last 1-indexed last page (`Paginator.last_page`)
48
- * @param window Number of neighbors on EACH side of `current` to
49
- * show before the ellipsis kicks in. Default 2 gives
50
- * the canonical compact shape.
51
- */
52
- export function buildPageSequence(
53
- current: number,
54
- last: number,
55
- window: number = 2,
56
- ): Array<number | '…'> {
57
- if (last <= 1) return []
58
- const out: Array<number | '…'> = []
59
- // Window bounds, clamped to [2, last-1] so we don't double-emit 1 or last.
60
- const lo = Math.max(2, current - window)
61
- const hi = Math.min(last - 1, current + window)
62
- out.push(1)
63
- // Only emit the leading ellipsis when the gap is >1 page wide; a gap of
64
- // exactly 1 (e.g. lo=3, hiding only page 2) is just shown as the real
65
- // page number — the ellipsis would be wider on screen than the digit it
66
- // replaces, and clicking it does nothing.
67
- if (lo === 3) out.push(2)
68
- else if (lo > 3) out.push('…')
69
- for (let i = lo; i <= hi; i++) out.push(i)
70
- // Symmetric for the trailing side.
71
- if (hi === last - 2) out.push(last - 1)
72
- else if (hi < last - 2) out.push('…')
73
- if (last > 1) out.push(last)
74
- return out
75
- }
76
-
77
- /**
78
- * Compute the URL for a specific page number, re-templating the
79
- * `page=N` parameter on whichever existing paginator URL is present.
80
- * Preserves all other query params (search filters, sort, etc.) — the
81
- * URLs filled in by `enrichPaginatorUrls()` (P2) already carry them,
82
- * so the re-template just swaps the page number.
83
- *
84
- * Falls back to `?page=N` when no template URL is available — covers
85
- * the case where the paginator was built outside a request scope (CLI
86
- * / queue / cron) and rendered via a non-default view; produces a
87
- * relative link that still works against the active page.
88
- */
89
- export function urlForPage(view: PaginatorView, page: number): string {
90
- const template = view.next_page_url || view.prev_page_url || view.first_page_url || view.last_page_url
91
- if (template) {
92
- if (/[?&]page=\d+/.test(template))
93
- return template.replace(/([?&])page=\d+/, `$1page=${page}`)
94
- // Template has no page= param yet (rare — paginator built without P2
95
- // enrichment but a URL was attached manually); append it.
96
- return `${template}${template.includes('?') ? '&' : '?'}page=${page}`
97
- }
98
- return `?page=${page}`
99
- }
100
-
101
- /**
102
- * Classify a paginator instance by shape so the view picks the right
103
- * UI variant. Returns one of `'full'` / `'simple'` / `'cursor'` based
104
- * on which fields are present. This mirrors `isPaginator` /
105
- * `isSimplePaginator` / `isCursorPaginator` in `@stacksjs/orm` but
106
- * lives here so the view layer doesn't need to import the orm.
107
- */
108
- export function paginatorVariant(p: unknown): 'full' | 'simple' | 'cursor' | 'unknown' {
109
- if (p === null || typeof p !== 'object') return 'unknown'
110
- const v = p as Record<string, unknown>
111
- if ('next_cursor' in v) return 'cursor'
112
- if ('total' in v && 'last_page' in v) return 'full'
113
- if ('current_page' in v && 'per_page' in v) return 'simple'
114
- return 'unknown'
115
- }