@taqwim/solid 0.1.0-beta.0

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2019 boussadjra
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,131 @@
1
+ # @taqwim/solid
2
+
3
+ Headless Solid components for Hijri calendars. No styles, no markup opinions — just behaviour, accessibility and the `data-*` attributes to hang a design on.
4
+
5
+ Want something that already looks like a calendar? Use [`@taqwim/solid-styled`](../solid-styled).
6
+
7
+ ## Install
8
+
9
+ ```sh
10
+ pnpm add @taqwim/solid
11
+ ```
12
+
13
+ Solid 1.8 or later.
14
+
15
+ ## Use
16
+
17
+ ```tsx
18
+ import {
19
+ HijriCalendarCell,
20
+ HijriCalendarCellTrigger,
21
+ HijriCalendarGrid,
22
+ HijriCalendarGridBody,
23
+ HijriCalendarGridHead,
24
+ HijriCalendarGridRow,
25
+ HijriCalendarHeadCell,
26
+ HijriCalendarHeader,
27
+ HijriCalendarHeading,
28
+ HijriCalendarNext,
29
+ HijriCalendarPrev,
30
+ HijriCalendarRoot,
31
+ } from '@taqwim/solid'
32
+ import { createSignal, For, Index } from 'solid-js'
33
+
34
+ export function Calendar() {
35
+ const [value, setValue] = createSignal()
36
+
37
+ return (
38
+ <HijriCalendarRoot value={value()} onValueChange={setValue} initialFocus>
39
+ {({ months, weekDays }) => (
40
+ <>
41
+ <HijriCalendarHeader>
42
+ <HijriCalendarPrev>←</HijriCalendarPrev>
43
+ <HijriCalendarHeading />
44
+ <HijriCalendarNext>→</HijriCalendarNext>
45
+ </HijriCalendarHeader>
46
+
47
+ <Index each={months}>
48
+ {month => (
49
+ <HijriCalendarGrid month={month()}>
50
+ <HijriCalendarGridHead>
51
+ <HijriCalendarGridRow>
52
+ <For each={weekDays}>{day => <HijriCalendarHeadCell>{day}</HijriCalendarHeadCell>}</For>
53
+ </HijriCalendarGridRow>
54
+ </HijriCalendarGridHead>
55
+
56
+ <HijriCalendarGridBody>
57
+ <Index each={month().weeks}>
58
+ {week => (
59
+ <HijriCalendarGridRow>
60
+ <Index each={week()}>
61
+ {day => (
62
+ <HijriCalendarCell day={day()}>
63
+ <HijriCalendarCellTrigger day={day()} />
64
+ </HijriCalendarCell>
65
+ )}
66
+ </Index>
67
+ </HijriCalendarGridRow>
68
+ )}
69
+ </Index>
70
+ </HijriCalendarGridBody>
71
+ </HijriCalendarGrid>
72
+ )}
73
+ </Index>
74
+ </>
75
+ )}
76
+ </HijriCalendarRoot>
77
+ )
78
+ }
79
+ ```
80
+
81
+ Use `Index` rather than `For` for the grid. The store rebuilds its arrays on every snapshot and `For` keys by reference, so it would throw away and recreate every cell on each change.
82
+
83
+ ## How it works
84
+
85
+ All behaviour lives in [`@taqwim/calendar-core`](../calendar-core), a framework-free state machine. `HijriCalendarRoot` mirrors the store's snapshot into a signal and shares it through Solid context; every part spreads attributes the store computed.
86
+
87
+ That is why the Vue, React, Svelte and Angular adapters behave identically: one implementation of grid layout, selection, paging and keyboard navigation, and one set of emitted attributes.
88
+
89
+ Drop to the store directly with `createCalendarStore`:
90
+
91
+ ```ts
92
+ import { createCalendarStore } from '@taqwim/solid'
93
+
94
+ const { store, state } = createCalendarStore(() => ({ locale: 'ar', dir: 'rtl' }))
95
+ ```
96
+
97
+ The accessor is a function so that reading props inside it registers them as dependencies — Solid props are getters, and destructuring would freeze the calendar at its initial configuration.
98
+
99
+ The package ships uncompiled JSX under the `solid` export condition so your compiler can target DOM or SSR; `dist/index.js` is a DOM-compiled fallback for bundlers that ignore the condition.
100
+
101
+ ## Props
102
+
103
+ The same names every adapter uses: `defaultValue` · `value` · `onValueChange` · `defaultPlaceholder` · `placeholder` · `onPlaceholderChange` · `weekStartsOn` · `weekdayFormat` · `numberOfMonths` · `pagedNavigation` · `fixedWeeks` · `multiple` · `preventDeselect` · `minValue` · `maxValue` · `isDateDisabled` · `isDateUnavailable` · `disableDaysOutsideCurrentView` · `disabled` · `readonly` · `locale` · `dir` · `initialFocus` · `nextPage` · `prevPage` · `calendarLabel`.
104
+
105
+ `minValue`/`maxValue` disable the out-of-range days themselves, not only the paging buttons, and the matchers are enforced for keyboard selection as well as clicks.
106
+
107
+ Full reference: [Calendar options](https://boussadjra.github.io/taqwim/reference/options/).
108
+
109
+ ## Keyboard
110
+
111
+ | Key | |
112
+ | ----------------------------- | ------------------------------------------------ |
113
+ | `←` `→` | Previous / next day — mirrored under `dir="rtl"` |
114
+ | `↑` `↓` | Previous / next week |
115
+ | `Home` `End` | First / last day of the week |
116
+ | `PageUp` `PageDown` | Previous / next month |
117
+ | `Shift` + `PageUp`/`PageDown` | Previous / next year |
118
+ | `Enter` `Space` | Select the focused day |
119
+
120
+ A roving tabindex keeps exactly one cell in the tab order, so `Tab` enters and leaves the grid rather than walking 42 buttons.
121
+
122
+ ## Attributes to style against
123
+
124
+ Root `[data-taqwim-calendar]`, with `data-disabled`, `data-readonly`, `data-invalid`.
125
+ Cell trigger `[data-taqwim-calendar-cell-trigger]`, with `data-selected`, `data-today`, `data-outside-month`, `data-disabled`, `data-unavailable`, `data-focused`, `data-value` (`iYYYY-iMM-iDD`).
126
+
127
+ [`@taqwim/themes`](../themes) is a ready-made stylesheet over exactly these.
128
+
129
+ ## License
130
+
131
+ MIT
package/dist/index.js ADDED
@@ -0,0 +1,221 @@
1
+ import { createComponent as e, insert as t, memo as n, mergeProps as r, spread as i, template as a, use as o } from "solid-js/web";
2
+ import { createContext as s, createEffect as c, createRenderEffect as l, createSignal as u, onCleanup as d, onMount as f, splitProps as p, useContext as m } from "solid-js";
3
+ import { createCalendar as h } from "@taqwim/calendar-core";
4
+ //#region src/context.ts
5
+ var g = s();
6
+ function _() {
7
+ let e = m(g);
8
+ if (!e) throw Error("Taqwim calendar parts must be rendered inside <HijriCalendarRoot>");
9
+ return e;
10
+ }
11
+ //#endregion
12
+ //#region src/types.ts
13
+ var v = [
14
+ "defaultValue",
15
+ "value",
16
+ "onValueChange",
17
+ "defaultPlaceholder",
18
+ "placeholder",
19
+ "onPlaceholderChange",
20
+ "weekStartsOn",
21
+ "weekdayFormat",
22
+ "calendarLabel",
23
+ "fixedWeeks",
24
+ "numberOfMonths",
25
+ "pagedNavigation",
26
+ "multiple",
27
+ "preventDeselect",
28
+ "disableDaysOutsideCurrentView",
29
+ "disabled",
30
+ "readonly",
31
+ "minValue",
32
+ "maxValue",
33
+ "locale",
34
+ "dir",
35
+ "isDateDisabled",
36
+ "isDateUnavailable",
37
+ "nextPage",
38
+ "prevPage"
39
+ ];
40
+ //#endregion
41
+ //#region src/useCalendar.ts
42
+ function y(e) {
43
+ let t = h(e()), [n, r] = u(t.getSnapshot());
44
+ return d(t.subscribe(() => {
45
+ r(t.getSnapshot());
46
+ })), l(() => {
47
+ t.setOptions(e());
48
+ }), {
49
+ store: t,
50
+ state: n
51
+ };
52
+ }
53
+ //#endregion
54
+ //#region src/HijriCalendar.tsx
55
+ var b = /*#__PURE__*/ a("<div><div style=position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap;border:0><div role=heading aria-level=2>"), x = /*#__PURE__*/ a("<div role=group data-taqwim-calendar-header>"), S = /*#__PURE__*/ a("<div data-taqwim-calendar-heading>"), C = /*#__PURE__*/ a("<button>"), w = /*#__PURE__*/ a("<div tabindex=-1>"), T = /*#__PURE__*/ a("<div aria-hidden=true data-taqwim-calendar-grid-head>"), E = /*#__PURE__*/ a("<div data-taqwim-calendar-grid-body>"), D = /*#__PURE__*/ a("<div role=row data-taqwim-calendar-grid-row>"), O = /*#__PURE__*/ a("<div data-taqwim-calendar-head-cell>"), k = /*#__PURE__*/ a("<div role=gridcell data-taqwim-calendar-cell>");
56
+ function A(e, t) {
57
+ return typeof e == "function" ? e(t()) : e;
58
+ }
59
+ function j(n) {
60
+ let [a, s, l] = p(n, v, ["children", "initialFocus"]), u, d, { store: m, state: h } = y(() => ({
61
+ ...a,
62
+ onFocusedDateChange: (e) => {
63
+ d = e ? m.formatter.isoDate(e) : void 0;
64
+ }
65
+ }));
66
+ c(() => {
67
+ if (h(), !d) return;
68
+ let e = d;
69
+ d = void 0, u?.querySelector(`[data-taqwim-calendar-cell-trigger][data-value="${e}"]`)?.focus();
70
+ }), f(() => {
71
+ s.initialFocus && m.focusInitial();
72
+ });
73
+ let _ = (e) => {
74
+ m.handleKeydown(e) && e.preventDefault();
75
+ }, x = () => (h(), m.getRootProps());
76
+ return e(g.Provider, {
77
+ value: {
78
+ store: m,
79
+ state: h
80
+ },
81
+ get children() {
82
+ var e = b(), n = e.firstChild, a = n.firstChild, c = u;
83
+ return typeof c == "function" ? o(c, e) : u = e, i(e, r(x, l, { onKeyDown: _ }), !1, !0), t(e, () => A(s.children, () => ({
84
+ get months() {
85
+ return h().months;
86
+ },
87
+ get weekDays() {
88
+ return h().weekDays;
89
+ },
90
+ get state() {
91
+ return h();
92
+ },
93
+ store: m
94
+ })), n), t(a, () => h().fullCalendarLabel), e;
95
+ }
96
+ });
97
+ }
98
+ function M(e) {
99
+ let { state: n } = _(), [a, o] = p(e, ["children"]);
100
+ return (() => {
101
+ var e = x();
102
+ return i(e, r({ get "aria-label"() {
103
+ return n().fullCalendarLabel;
104
+ } }, o), !1, !0), t(e, () => a.children), e;
105
+ })();
106
+ }
107
+ function N(e) {
108
+ let { state: a } = _(), [o, s] = p(e, ["children"]);
109
+ return (() => {
110
+ var e = S();
111
+ return i(e, r({ get "data-disabled"() {
112
+ return a().disabled ? "" : void 0;
113
+ } }, s), !1, !0), t(e, (() => {
114
+ var e = n(() => o.children === void 0);
115
+ return () => e() ? a().headingValue : A(o.children, () => a().headingValue);
116
+ })()), e;
117
+ })();
118
+ }
119
+ function P(e) {
120
+ let { store: n, state: a } = _(), [o, s] = p(e, ["children"]), c = () => (a(), n.getPrevButtonProps());
121
+ return (() => {
122
+ var e = C();
123
+ return i(e, r(c, s, {
124
+ get disabled() {
125
+ return a().isPrevDisabled;
126
+ },
127
+ onClick: () => n.prevPage()
128
+ }), !1, !0), t(e, () => A(o.children, () => a().isPrevDisabled)), e;
129
+ })();
130
+ }
131
+ function F(e) {
132
+ let { store: n, state: a } = _(), [o, s] = p(e, ["children"]), c = () => (a(), n.getNextButtonProps());
133
+ return (() => {
134
+ var e = C();
135
+ return i(e, r(c, s, {
136
+ get disabled() {
137
+ return a().isNextDisabled;
138
+ },
139
+ onClick: () => n.nextPage()
140
+ }), !1, !0), t(e, () => A(o.children, () => a().isNextDisabled)), e;
141
+ })();
142
+ }
143
+ function I(e) {
144
+ let { store: n, state: a } = _(), [o, s] = p(e, ["month", "children"]), c = () => o.month ?? a().months[0], l = () => (a(), n.getGridProps(c()));
145
+ return (() => {
146
+ var e = w();
147
+ return i(e, r(l, s), !1, !0), t(e, () => A(o.children, c)), e;
148
+ })();
149
+ }
150
+ function L(e) {
151
+ let { state: n } = _(), [r, a] = p(e, ["children"]);
152
+ return (() => {
153
+ var e = T();
154
+ return i(e, a, !1, !0), t(e, () => A(r.children, () => n().weekDays)), e;
155
+ })();
156
+ }
157
+ function R(e) {
158
+ let [n, r] = p(e, ["children"]);
159
+ return (() => {
160
+ var e = E();
161
+ return i(e, r, !1, !0), t(e, () => n.children), e;
162
+ })();
163
+ }
164
+ function z(e) {
165
+ let [n, r] = p(e, ["children"]);
166
+ return (() => {
167
+ var e = D();
168
+ return i(e, r, !1, !0), t(e, () => n.children), e;
169
+ })();
170
+ }
171
+ function B(e) {
172
+ let [n, r] = p(e, ["children"]);
173
+ return (() => {
174
+ var e = O();
175
+ return i(e, r, !1, !0), t(e, () => n.children), e;
176
+ })();
177
+ }
178
+ function V(e) {
179
+ let [n, a] = p(e, ["day", "children"]);
180
+ return (() => {
181
+ var e = k();
182
+ return i(e, r({
183
+ get "aria-selected"() {
184
+ return n.day.isSelected || void 0;
185
+ },
186
+ get "aria-disabled"() {
187
+ return n.day.isDisabled || n.day.isUnavailable || void 0;
188
+ },
189
+ get "data-disabled"() {
190
+ return n.day.isDisabled ? "" : void 0;
191
+ },
192
+ get "data-outside-month"() {
193
+ return n.day.isOutsideMonth ? "" : void 0;
194
+ }
195
+ }, a), !1, !0), t(e, () => n.children), e;
196
+ })();
197
+ }
198
+ function H(e) {
199
+ let { store: a, state: o } = _(), [s, c] = p(e, ["day", "children"]), l = () => a.formatter.dayOfMonth(s.day.date), u = () => (o(), a.getCellTriggerProps(s.day));
200
+ function d() {
201
+ s.day.isDisabled || s.day.isUnavailable || a.select(s.day.date);
202
+ }
203
+ function f() {
204
+ s.day.isDisabled || s.day.isFocused || a.focusDate(s.day.date);
205
+ }
206
+ return (() => {
207
+ var e = C();
208
+ return i(e, r(u, c, {
209
+ onClick: d,
210
+ onFocus: f
211
+ }), !1, !0), t(e, (() => {
212
+ var e = n(() => s.children === void 0);
213
+ return () => e() ? l() : A(s.children, () => ({
214
+ dayValue: l(),
215
+ day: s.day
216
+ }));
217
+ })()), e;
218
+ })();
219
+ }
220
+ //#endregion
221
+ export { V as HijriCalendarCell, H as HijriCalendarCellTrigger, I as HijriCalendarGrid, R as HijriCalendarGridBody, L as HijriCalendarGridHead, z as HijriCalendarGridRow, B as HijriCalendarHeadCell, M as HijriCalendarHeader, N as HijriCalendarHeading, F as HijriCalendarNext, P as HijriCalendarPrev, j as HijriCalendarRoot, y as createCalendarStore, _ as useHijriCalendarContext };
@@ -0,0 +1,47 @@
1
+ import type { CalendarDay, CalendarMonth } from '@taqwim/calendar-core';
2
+ import { type JSX } from 'solid-js';
3
+ import { type HijriCalendarRootProps } from './types';
4
+ type DivProps = Omit<JSX.HTMLAttributes<HTMLDivElement>, 'children'>;
5
+ type ButtonProps = Omit<JSX.ButtonHTMLAttributes<HTMLButtonElement>, 'children' | 'onClick' | 'type'>;
6
+ export declare function HijriCalendarRoot(props: HijriCalendarRootProps): JSX.Element;
7
+ export declare function HijriCalendarHeader(props: DivProps & {
8
+ children?: JSX.Element;
9
+ }): JSX.Element;
10
+ export declare function HijriCalendarHeading(props: DivProps & {
11
+ children?: JSX.Element | ((headingValue: string) => JSX.Element);
12
+ }): JSX.Element;
13
+ export declare function HijriCalendarPrev(props: ButtonProps & {
14
+ children?: JSX.Element | ((disabled: boolean) => JSX.Element);
15
+ }): JSX.Element;
16
+ export declare function HijriCalendarNext(props: ButtonProps & {
17
+ children?: JSX.Element | ((disabled: boolean) => JSX.Element);
18
+ }): JSX.Element;
19
+ export declare function HijriCalendarGrid(props: DivProps & {
20
+ /** Optional for the single-month case, where it defaults to the only month. */
21
+ month?: CalendarMonth;
22
+ children?: JSX.Element | ((month: CalendarMonth) => JSX.Element);
23
+ }): JSX.Element;
24
+ export declare function HijriCalendarGridHead(props: DivProps & {
25
+ children?: JSX.Element | ((weekDays: string[]) => JSX.Element);
26
+ }): JSX.Element;
27
+ export declare function HijriCalendarGridBody(props: DivProps & {
28
+ children?: JSX.Element;
29
+ }): JSX.Element;
30
+ export declare function HijriCalendarGridRow(props: DivProps & {
31
+ children?: JSX.Element;
32
+ }): JSX.Element;
33
+ export declare function HijriCalendarHeadCell(props: DivProps & {
34
+ children?: JSX.Element;
35
+ }): JSX.Element;
36
+ export declare function HijriCalendarCell(props: DivProps & {
37
+ day: CalendarDay;
38
+ children?: JSX.Element;
39
+ }): JSX.Element;
40
+ export declare function HijriCalendarCellTrigger(props: ButtonProps & {
41
+ day: CalendarDay;
42
+ children?: JSX.Element | ((props: {
43
+ dayValue: string;
44
+ day: CalendarDay;
45
+ }) => JSX.Element);
46
+ }): JSX.Element;
47
+ export {};
@@ -0,0 +1,227 @@
1
+ import { createEffect, onMount, splitProps } from 'solid-js';
2
+ import { HijriCalendarContext, useHijriCalendarContext } from './context';
3
+ import { OPTION_KEYS } from './types';
4
+ import { createCalendarStore } from './useCalendar';
5
+ /*
6
+ * Every part below spreads props the store computed. Because those are the same
7
+ * `data-*` and `aria-*` attributes the Vue and React adapters emit,
8
+ * `@taqwim/themes` styles all of them and one Playwright spec covers all of
9
+ * them.
10
+ *
11
+ * Props are never destructured — Solid props are getters, and destructuring
12
+ * would read them once and lose reactivity.
13
+ */
14
+ /*
15
+ * `props` is an accessor, not a value, and that is the whole point.
16
+ *
17
+ * Every call site sits inside a JSX expression, which Solid compiles into a
18
+ * tracked computation. Passing `month()` or `state().weekDays` evaluates the
19
+ * signal *to build the argument*, which subscribes the computation to it — so
20
+ * every store change threw the subtree away and built a new one, even for the
21
+ * common case where `children` is plain JSX and the argument is never used.
22
+ *
23
+ * For the grid that was not a performance detail. A browser focuses a button
24
+ * before it clicks it, the focus handler calls `store.focusDate`, and Solid
25
+ * delegates `click` to the document: the click was dispatched at a button that
26
+ * had already been replaced, and selection by mouse did not work at all.
27
+ *
28
+ * Read lazily, the argument is only built when a caller actually passed a
29
+ * function, and the JSX subtree is created once and then updated in place by
30
+ * the fine-grained bindings inside it.
31
+ */
32
+ function renderChildren(children, props) {
33
+ return typeof children === 'function' ? children(props()) : children;
34
+ }
35
+ export function HijriCalendarRoot(props) {
36
+ const [options, local, domProps] = splitProps(props, OPTION_KEYS, ['children', 'initialFocus']);
37
+ let rootElement;
38
+ // Set by the store when the roving focus moves; consumed after the render.
39
+ let pendingFocus;
40
+ const { store, state } = createCalendarStore(() => ({
41
+ ...options,
42
+ onFocusedDateChange: date => {
43
+ pendingFocus = date ? store.formatter.isoDate(date) : undefined;
44
+ },
45
+ }));
46
+ /*
47
+ * The store owns which date has focus; the adapter owns the DOM. Solid's
48
+ * effects run after the DOM is updated, so the target cell exists even when
49
+ * the move paged the calendar into a month that was not rendered a moment
50
+ * ago.
51
+ */
52
+ createEffect(() => {
53
+ // Read the state so this re-runs on every change.
54
+ state();
55
+ if (!pendingFocus)
56
+ return;
57
+ const value = pendingFocus;
58
+ pendingFocus = undefined;
59
+ rootElement?.querySelector(`[data-taqwim-calendar-cell-trigger][data-value="${value}"]`)?.focus();
60
+ });
61
+ onMount(() => {
62
+ if (local.initialFocus)
63
+ store.focusInitial();
64
+ });
65
+ const onKeyDown = event => {
66
+ if (store.handleKeydown(event))
67
+ event.preventDefault();
68
+ };
69
+ const rootProps = () => {
70
+ void state();
71
+ return store.getRootProps();
72
+ };
73
+ return (<HijriCalendarContext.Provider value={{ store, state }}>
74
+ <div ref={rootElement} {...rootProps()} {...domProps} onKeyDown={onKeyDown}>
75
+ {renderChildren(local.children, () => ({
76
+ get months() {
77
+ return state().months;
78
+ },
79
+ get weekDays() {
80
+ return state().weekDays;
81
+ },
82
+ get state() {
83
+ return state();
84
+ },
85
+ store,
86
+ }))}
87
+
88
+ {/* Inlined rather than classed: the headless package must not require a stylesheet. */}
89
+ <div style={{
90
+ position: 'absolute',
91
+ width: '1px',
92
+ height: '1px',
93
+ padding: '0',
94
+ margin: '-1px',
95
+ overflow: 'hidden',
96
+ 'clip-path': 'inset(50%)',
97
+ 'white-space': 'nowrap',
98
+ border: '0',
99
+ }}>
100
+ <div role="heading" aria-level={2}>
101
+ {state().fullCalendarLabel}
102
+ </div>
103
+ </div>
104
+ </div>
105
+ </HijriCalendarContext.Provider>);
106
+ }
107
+ export function HijriCalendarHeader(props) {
108
+ const { state } = useHijriCalendarContext();
109
+ const [local, rest] = splitProps(props, ['children']);
110
+ return (<div role="group" aria-label={state().fullCalendarLabel} data-taqwim-calendar-header="" {...rest}>
111
+ {local.children}
112
+ </div>);
113
+ }
114
+ export function HijriCalendarHeading(props) {
115
+ const { state } = useHijriCalendarContext();
116
+ const [local, rest] = splitProps(props, ['children']);
117
+ return (<div data-taqwim-calendar-heading="" data-disabled={state().disabled ? '' : undefined} {...rest}>
118
+ {local.children === undefined ? state().headingValue : renderChildren(local.children, () => state().headingValue)}
119
+ </div>);
120
+ }
121
+ export function HijriCalendarPrev(props) {
122
+ const { store, state } = useHijriCalendarContext();
123
+ const [local, rest] = splitProps(props, ['children']);
124
+ const prevProps = () => {
125
+ void state();
126
+ return store.getPrevButtonProps();
127
+ };
128
+ return (<button {...prevProps()} {...rest} disabled={state().isPrevDisabled} onClick={() => store.prevPage()}>
129
+ {renderChildren(local.children, () => state().isPrevDisabled)}
130
+ </button>);
131
+ }
132
+ export function HijriCalendarNext(props) {
133
+ const { store, state } = useHijriCalendarContext();
134
+ const [local, rest] = splitProps(props, ['children']);
135
+ const nextProps = () => {
136
+ void state();
137
+ return store.getNextButtonProps();
138
+ };
139
+ return (<button {...nextProps()} {...rest} disabled={state().isNextDisabled} onClick={() => store.nextPage()}>
140
+ {renderChildren(local.children, () => state().isNextDisabled)}
141
+ </button>);
142
+ }
143
+ export function HijriCalendarGrid(props) {
144
+ const { store, state } = useHijriCalendarContext();
145
+ const [local, rest] = splitProps(props, ['month', 'children']);
146
+ const month = () => local.month ?? state().months[0];
147
+ const gridProps = () => {
148
+ void state();
149
+ return store.getGridProps(month());
150
+ };
151
+ return (<div tabindex={-1} {...gridProps()} {...rest}>
152
+ {/*
153
+ `month`, not `month()`. Handing over the accessor keeps this expression
154
+ from reading the store, so the grid is built once and then updated in
155
+ place; passing the value rebuilt every cell on every state change and
156
+ broke selection by mouse outright. See `renderChildren` above.
157
+ */}
158
+ {renderChildren(local.children, month)}
159
+ </div>);
160
+ }
161
+ export function HijriCalendarGridHead(props) {
162
+ const { state } = useHijriCalendarContext();
163
+ const [local, rest] = splitProps(props, ['children']);
164
+ // The weekday row duplicates each cell's accessible label, so it is hidden
165
+ // from assistive technology rather than read out twice.
166
+ return (<div aria-hidden="true" data-taqwim-calendar-grid-head="" {...rest}>
167
+ {renderChildren(local.children, () => state().weekDays)}
168
+ </div>);
169
+ }
170
+ export function HijriCalendarGridBody(props) {
171
+ const [local, rest] = splitProps(props, ['children']);
172
+ return (<div data-taqwim-calendar-grid-body="" {...rest}>
173
+ {local.children}
174
+ </div>);
175
+ }
176
+ export function HijriCalendarGridRow(props) {
177
+ const [local, rest] = splitProps(props, ['children']);
178
+ return (<div role="row" data-taqwim-calendar-grid-row="" {...rest}>
179
+ {local.children}
180
+ </div>);
181
+ }
182
+ export function HijriCalendarHeadCell(props) {
183
+ const [local, rest] = splitProps(props, ['children']);
184
+ return (<div data-taqwim-calendar-head-cell="" {...rest}>
185
+ {local.children}
186
+ </div>);
187
+ }
188
+ export function HijriCalendarCell(props) {
189
+ const [local, rest] = splitProps(props, ['day', 'children']);
190
+ return (<div role="gridcell" aria-selected={local.day.isSelected || undefined} aria-disabled={local.day.isDisabled || local.day.isUnavailable || undefined} data-taqwim-calendar-cell="" data-disabled={local.day.isDisabled ? '' : undefined} data-outside-month={local.day.isOutsideMonth ? '' : undefined} {...rest}>
191
+ {local.children}
192
+ </div>);
193
+ }
194
+ export function HijriCalendarCellTrigger(props) {
195
+ const { store, state } = useHijriCalendarContext();
196
+ const [local, rest] = splitProps(props, ['day', 'children']);
197
+ const dayValue = () => store.formatter.dayOfMonth(local.day.date);
198
+ /*
199
+ * The prop getters read the store's snapshot directly, which is not a signal.
200
+ * Touching `state()` here is what makes this a tracked dependency — without
201
+ * it the attributes only refresh when the day object happens to change
202
+ * identity, which leaves the rendered selection one interaction behind.
203
+ */
204
+ const triggerProps = () => {
205
+ void state();
206
+ return store.getCellTriggerProps(local.day);
207
+ };
208
+ function onClick() {
209
+ // `select` re-checks these itself; this only avoids the pointless call.
210
+ if (local.day.isDisabled || local.day.isUnavailable)
211
+ return;
212
+ store.select(local.day.date);
213
+ }
214
+ function onFocus() {
215
+ // Tabbing or clicking into a cell makes it the roving-focus target.
216
+ // Re-reporting a date the store already holds would echo its own
217
+ // programmatic `.focus()` back at it, so that case is skipped.
218
+ if (local.day.isDisabled || local.day.isFocused)
219
+ return;
220
+ store.focusDate(local.day.date);
221
+ }
222
+ return (<button {...triggerProps()} {...rest} onClick={onClick} onFocus={onFocus}>
223
+ {local.children === undefined
224
+ ? dayValue()
225
+ : renderChildren(local.children, () => ({ dayValue: dayValue(), day: local.day }))}
226
+ </button>);
227
+ }
@@ -0,0 +1,9 @@
1
+ import type { CalendarState, CalendarStore } from '@taqwim/calendar-core';
2
+ import { type Accessor } from 'solid-js';
3
+ export interface HijriCalendarContextValue {
4
+ store: CalendarStore;
5
+ state: Accessor<CalendarState>;
6
+ }
7
+ declare const HijriCalendarContext: import("solid-js").Context<HijriCalendarContextValue | undefined>;
8
+ export { HijriCalendarContext };
9
+ export declare function useHijriCalendarContext(): HijriCalendarContextValue;
@@ -0,0 +1,10 @@
1
+ import { createContext, useContext } from 'solid-js';
2
+ const HijriCalendarContext = createContext();
3
+ export { HijriCalendarContext };
4
+ export function useHijriCalendarContext() {
5
+ const context = useContext(HijriCalendarContext);
6
+ if (!context) {
7
+ throw new Error('Taqwim calendar parts must be rendered inside <HijriCalendarRoot>');
8
+ }
9
+ return context;
10
+ }
@@ -0,0 +1,5 @@
1
+ export { HijriCalendarCell, HijriCalendarCellTrigger, HijriCalendarGrid, HijriCalendarGridBody, HijriCalendarGridHead, HijriCalendarGridRow, HijriCalendarHeadCell, HijriCalendarHeader, HijriCalendarHeading, HijriCalendarNext, HijriCalendarPrev, HijriCalendarRoot, } from './HijriCalendar';
2
+ export { useHijriCalendarContext, type HijriCalendarContextValue } from './context';
3
+ export type { HijriCalendarRenderProps, HijriCalendarRootOptions, HijriCalendarRootProps } from './types';
4
+ export { createCalendarStore, type UseCalendarReturn } from './useCalendar';
5
+ export type { CalendarDay, CalendarFormatter, CalendarMonth, CalendarOptions, CalendarState, CalendarStore, Direction, Matcher, WeekDayFormat, WeekStartsOn, } from '@taqwim/calendar-core';
@@ -0,0 +1,3 @@
1
+ export { HijriCalendarCell, HijriCalendarCellTrigger, HijriCalendarGrid, HijriCalendarGridBody, HijriCalendarGridHead, HijriCalendarGridRow, HijriCalendarHeadCell, HijriCalendarHeader, HijriCalendarHeading, HijriCalendarNext, HijriCalendarPrev, HijriCalendarRoot, } from './HijriCalendar';
2
+ export { useHijriCalendarContext } from './context';
3
+ export { createCalendarStore } from './useCalendar';
@@ -0,0 +1,55 @@
1
+ import type { CalendarMonth, CalendarState, CalendarStore, Matcher, WeekDayFormat, WeekStartsOn } from '@taqwim/calendar-core';
2
+ import type { HijriDateObject } from '@taqwim/core';
3
+ import type { JSX } from 'solid-js';
4
+ /**
5
+ * Deliberately the same names as the Vue and React adapters.
6
+ *
7
+ * Parity is a hard requirement — the shared end-to-end suite runs one spec file
8
+ * against every framework — so the surfaces differ only where the host
9
+ * framework forces it.
10
+ */
11
+ export interface HijriCalendarRootOptions {
12
+ /** Initial selection, for uncontrolled use. */
13
+ defaultValue?: HijriDateObject | HijriDateObject[];
14
+ /** Controlled selection. */
15
+ value?: HijriDateObject | HijriDateObject[] | undefined;
16
+ onValueChange?: (value: HijriDateObject | HijriDateObject[] | undefined) => void;
17
+ /** Month to display when nothing is selected. */
18
+ defaultPlaceholder?: HijriDateObject;
19
+ /** Controlled placeholder. */
20
+ placeholder?: HijriDateObject;
21
+ onPlaceholderChange?: (placeholder: HijriDateObject) => void;
22
+ weekStartsOn?: WeekStartsOn;
23
+ weekdayFormat?: WeekDayFormat;
24
+ calendarLabel?: string;
25
+ fixedWeeks?: boolean;
26
+ numberOfMonths?: number;
27
+ pagedNavigation?: boolean;
28
+ multiple?: boolean;
29
+ preventDeselect?: boolean;
30
+ disableDaysOutsideCurrentView?: boolean;
31
+ disabled?: boolean;
32
+ readonly?: boolean;
33
+ minValue?: HijriDateObject;
34
+ maxValue?: HijriDateObject;
35
+ locale?: string;
36
+ dir?: 'ltr' | 'rtl';
37
+ isDateDisabled?: Matcher;
38
+ isDateUnavailable?: Matcher;
39
+ nextPage?: (placeholder: HijriDateObject) => HijriDateObject;
40
+ prevPage?: (placeholder: HijriDateObject) => HijriDateObject;
41
+ /** Focus the selected day, today, or the first of the month on mount. */
42
+ initialFocus?: boolean;
43
+ }
44
+ export interface HijriCalendarRenderProps {
45
+ months: CalendarMonth[];
46
+ weekDays: string[];
47
+ state: CalendarState;
48
+ store: CalendarStore;
49
+ }
50
+ export type HijriCalendarRootProps = HijriCalendarRootOptions & Omit<JSX.HTMLAttributes<HTMLDivElement>, 'children' | 'dir'> & {
51
+ /** A render function is the Solid equivalent of Vue's scoped slot. */
52
+ children?: JSX.Element | ((props: HijriCalendarRenderProps) => JSX.Element);
53
+ };
54
+ /** The option names, so the root can tell them from DOM attributes. */
55
+ export declare const OPTION_KEYS: readonly ["defaultValue", "value", "onValueChange", "defaultPlaceholder", "placeholder", "onPlaceholderChange", "weekStartsOn", "weekdayFormat", "calendarLabel", "fixedWeeks", "numberOfMonths", "pagedNavigation", "multiple", "preventDeselect", "disableDaysOutsideCurrentView", "disabled", "readonly", "minValue", "maxValue", "locale", "dir", "isDateDisabled", "isDateUnavailable", "nextPage", "prevPage"];
@@ -0,0 +1,28 @@
1
+ /** The option names, so the root can tell them from DOM attributes. */
2
+ export const OPTION_KEYS = [
3
+ 'defaultValue',
4
+ 'value',
5
+ 'onValueChange',
6
+ 'defaultPlaceholder',
7
+ 'placeholder',
8
+ 'onPlaceholderChange',
9
+ 'weekStartsOn',
10
+ 'weekdayFormat',
11
+ 'calendarLabel',
12
+ 'fixedWeeks',
13
+ 'numberOfMonths',
14
+ 'pagedNavigation',
15
+ 'multiple',
16
+ 'preventDeselect',
17
+ 'disableDaysOutsideCurrentView',
18
+ 'disabled',
19
+ 'readonly',
20
+ 'minValue',
21
+ 'maxValue',
22
+ 'locale',
23
+ 'dir',
24
+ 'isDateDisabled',
25
+ 'isDateUnavailable',
26
+ 'nextPage',
27
+ 'prevPage',
28
+ ];
@@ -0,0 +1,14 @@
1
+ import type { CalendarOptions, CalendarState, CalendarStore } from '@taqwim/calendar-core';
2
+ import { type Accessor } from 'solid-js';
3
+ export interface UseCalendarReturn {
4
+ store: CalendarStore;
5
+ state: Accessor<CalendarState>;
6
+ }
7
+ /**
8
+ * Binds a `@taqwim/calendar-core` store to Solid's reactivity.
9
+ *
10
+ * `options` is an accessor so that reading props inside it registers them as
11
+ * dependencies of the effect below — Solid props are getters, so destructuring
12
+ * them here would freeze the calendar at its initial configuration.
13
+ */
14
+ export declare function createCalendarStore(options: Accessor<CalendarOptions>): UseCalendarReturn;
@@ -0,0 +1,30 @@
1
+ import { createCalendar } from '@taqwim/calendar-core';
2
+ import { createRenderEffect, createSignal, onCleanup } from 'solid-js';
3
+ /**
4
+ * Binds a `@taqwim/calendar-core` store to Solid's reactivity.
5
+ *
6
+ * `options` is an accessor so that reading props inside it registers them as
7
+ * dependencies of the effect below — Solid props are getters, so destructuring
8
+ * them here would freeze the calendar at its initial configuration.
9
+ */
10
+ export function createCalendarStore(options) {
11
+ const store = createCalendar(options());
12
+ const [state, setState] = createSignal(store.getSnapshot());
13
+ onCleanup(store.subscribe(() => {
14
+ setState(store.getSnapshot());
15
+ }));
16
+ /*
17
+ * A render effect, not a plain effect: options have to reach the store
18
+ * before the components read the snapshot for this render. With
19
+ * `createEffect` the push lands after the DOM is written, so a controlled
20
+ * `value` handed back through `onValueChange` shows up one interaction late.
21
+ *
22
+ * Safe to run on every dependency change — the store compares the state it
23
+ * builds and stays quiet when nothing observable moved, so this cannot feed
24
+ * back into itself.
25
+ */
26
+ createRenderEffect(() => {
27
+ store.setOptions(options());
28
+ });
29
+ return { store, state };
30
+ }
package/package.json ADDED
@@ -0,0 +1,74 @@
1
+ {
2
+ "name": "@taqwim/solid",
3
+ "version": "0.1.0-beta.0",
4
+ "private": false,
5
+ "description": "Headless Solid components for Hijri calendars, built on @taqwim/calendar-core",
6
+ "keywords": [
7
+ "accessibility",
8
+ "calendar",
9
+ "datepicker",
10
+ "headless",
11
+ "hijri",
12
+ "islamic",
13
+ "solid",
14
+ "solid-js",
15
+ "taqwim",
16
+ "typescript"
17
+ ],
18
+ "license": "MIT",
19
+ "author": "Brahim Boussadjra",
20
+ "repository": {
21
+ "type": "git",
22
+ "url": "git+https://github.com/boussadjra/taqwim.git",
23
+ "directory": "packages/solid"
24
+ },
25
+ "files": [
26
+ "dist/**/*",
27
+ "CHANGELOG.md",
28
+ "LICENSE",
29
+ "README.md"
30
+ ],
31
+ "type": "module",
32
+ "sideEffects": false,
33
+ "main": "./dist/index.js",
34
+ "module": "./dist/index.js",
35
+ "types": "./dist/source/index.d.ts",
36
+ "exports": {
37
+ ".": {
38
+ "types": "./dist/source/index.d.ts",
39
+ "solid": "./dist/source/index.js",
40
+ "import": "./dist/index.js",
41
+ "default": "./dist/index.js"
42
+ },
43
+ "./package.json": "./package.json"
44
+ },
45
+ "publishConfig": {
46
+ "access": "public"
47
+ },
48
+ "dependencies": {
49
+ "@taqwim/calendar-core": "0.1.0-beta.0",
50
+ "@taqwim/core": "0.1.0-beta.0"
51
+ },
52
+ "devDependencies": {
53
+ "@arethetypeswrong/cli": "^0.18.2",
54
+ "@solidjs/testing-library": "^0.8.10",
55
+ "@testing-library/dom": "^10.4.0",
56
+ "jsdom": "^26.1.0",
57
+ "publint": "^0.3.14",
58
+ "solid-js": "^1.9.14",
59
+ "typescript": "~5.9.3",
60
+ "vite": "^8.1.5",
61
+ "vite-plugin-solid": "^2.11.14",
62
+ "vitest": "^4.1.10"
63
+ },
64
+ "peerDependencies": {
65
+ "solid-js": "^1.8.0"
66
+ },
67
+ "engines": {
68
+ "node": ">=20"
69
+ },
70
+ "scripts": {
71
+ "_comment": "build / test are declared as vp tasks in vite.config.ts",
72
+ "test:watch": "vp exec vitest"
73
+ }
74
+ }