tamga-ui 0.6.0 → 0.7.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 (44) hide show
  1. package/CHANGELOG.md +148 -0
  2. package/dist/blocks/danger-zone.d.ts +54 -0
  3. package/dist/blocks/danger-zone.d.ts.map +1 -0
  4. package/dist/blocks/danger-zone.js +18 -0
  5. package/dist/blocks/index.d.ts +1 -0
  6. package/dist/blocks/index.d.ts.map +1 -1
  7. package/dist/blocks/index.js +1 -0
  8. package/dist/components/appearance.d.ts +91 -5
  9. package/dist/components/appearance.d.ts.map +1 -1
  10. package/dist/components/appearance.js +164 -18
  11. package/dist/components/button.d.ts +2 -1
  12. package/dist/components/button.d.ts.map +1 -1
  13. package/dist/components/button.js +1 -1
  14. package/dist/components/chrome.d.ts +10 -1
  15. package/dist/components/chrome.d.ts.map +1 -1
  16. package/dist/components/chrome.js +2 -2
  17. package/dist/components/date-picker.d.ts +142 -4
  18. package/dist/components/date-picker.d.ts.map +1 -1
  19. package/dist/components/date-picker.js +336 -40
  20. package/dist/components/disclosure.d.ts +37 -2
  21. package/dist/components/disclosure.d.ts.map +1 -1
  22. package/dist/components/disclosure.js +13 -3
  23. package/dist/components/display.d.ts +35 -6
  24. package/dist/components/display.d.ts.map +1 -1
  25. package/dist/components/display.js +21 -7
  26. package/dist/components/input.d.ts +2 -1
  27. package/dist/components/input.d.ts.map +1 -1
  28. package/dist/components/primitives.d.ts +3 -1
  29. package/dist/components/primitives.d.ts.map +1 -1
  30. package/dist/components/primitives.js +2 -2
  31. package/dist/fonts.css +1 -1
  32. package/dist/index.d.ts +2 -2
  33. package/dist/index.d.ts.map +1 -1
  34. package/dist/index.js +2 -2
  35. package/dist/kit.css +115 -3
  36. package/dist/lib/image.d.ts +2 -0
  37. package/dist/lib/image.d.ts.map +1 -1
  38. package/dist/lib/image.js +1 -1
  39. package/dist/lib/uyar.d.ts +2 -0
  40. package/dist/lib/uyar.d.ts.map +1 -0
  41. package/dist/lib/uyar.js +10 -0
  42. package/dist/styles.css +1 -1
  43. package/dist/theme.css +8 -1
  44. package/package.json +10 -12
package/CHANGELOG.md CHANGED
@@ -5,6 +5,154 @@ Versions follow [semver](https://semver.org/). Through `0.x`, breaking changes m
5
5
 
6
6
  > 🇹🇷 Türkçe için [tamga.org.tr/tr](https://tamga.org.tr/tr).
7
7
 
8
+ ## 0.7.1
9
+
10
+ > An empty calendar opens where something can be picked.
11
+
12
+ ### Fixed
13
+
14
+ - **`Calendar` / `DatePicker` / `DateTimePicker` opened on today's month even when `min` sat
15
+ months ahead.** Every visible day was disabled and no cell carried `tabindex="0"`, so the
16
+ keyboard could not enter the grid and the mouse had to press "next month" for years (a
17
+ maintenance window whose end is bounded by a start in 2031 opened in 2026). The first month
18
+ is now the value's; without a value, today's if today is inside `[min, max]`, else the month
19
+ of `min` (of `max` when today is past it). The tab stop lands on the selected day or the first
20
+ open day of that month, as before. Only the first render decides: an open calendar does not
21
+ jump when `min`/`max` change from outside, the next opening follows the new bounds.
22
+
23
+ ## 0.7.0
24
+
25
+ > A calendar can say no and carry the hour; a section can be told whether it is open; a label can
26
+ > carry the user's colour; the settings page has a bottom; the image field can talk to a server.
27
+
28
+ ### Added
29
+
30
+ - **`DatePicker` / `Calendar` `min` and `max`** (`DateISO`). Days outside the range are disabled
31
+ and announced as such, the arrow keys skip them, and a month button closes when the whole
32
+ month is out of reach. A product had fallen back to `<input type="date" min=…>` for "not before
33
+ today"; that field looks different in every browser and lets the user type a date the kit had
34
+ decided should only be picked.
35
+ - **Calendar keyboard and grid semantics.** One day is in the tab order; the arrows move between
36
+ days (±1, ±7) across month edges, `Home`/`End` to the week's edges, `PageUp`/`PageDown` across
37
+ months; disabled days are skipped and the stop never lands on one (it follows the selected
38
+ day, else the first open day, and moves with an external `value`). `role="grid"` with rows
39
+ and `gridcell`s, every day named with its full date, `aria-selected` on the chosen day, native
40
+ `disabled` only.
41
+ - **`DateTimePicker`.** Date plus time, picked not typed: a calendar and hour/minute selects
42
+ (AM/PM only in 12-hour locales; the words from `Intl`, else `labels.am`/`labels.pm`, never
43
+ the kit's own English). The value is `DateTimeISO`, an instant written with the offset of the
44
+ picker's `timeZone` at that instant, `2026-04-15T14:30:00+03:00`; send
45
+ `new Date(v).toISOString()` to the server. A separate component rather than a prop: the
46
+ value's shape is different, and an `onChange` whose type depends on a prop is a trap.
47
+ `labels` adds `hour`, `minute`, `period`.
48
+ - `timeZone` (IANA) on `Calendar`, `DatePicker` and `DateTimePicker`. Wall clock and instant
49
+ meet through `Intl`; "today", `min`/`max`, the field's text and the emitted offset all use
50
+ that zone, never the browser's. Give it explicitly in a server-rendered app. DST: an hour
51
+ inside a spring gap is not offered and a wall time in it is pushed forward; an autumn
52
+ ambiguous hour is written with the later, standard offset, while a value coming from
53
+ outside keeps its own offset, also when only its minute changes. Checked against an
54
+ independent oracle over the 2026 transitions of 418 zones.
55
+ - Every emitted value is inside `[min, max]`: day, hour, minute and AM/PM changes are clamped
56
+ to the nearest valid time and the selects follow; hours with no valid minute are not
57
+ offered. A stored minute off the `minuteStep` grid, or a stored value outside the range, is
58
+ shown as it is rather than rounded or hidden: the field never lies about a stored value.
59
+ The hour boxes stay locked until a day is picked, so an hour alone never emits today.
60
+ `minuteStep` below 1 reads as 1.
61
+ - `id` and `invalid` on both pickers, like `Input`. An invalid `timeZone` falls back to the
62
+ browser's and an unparseable `value` / `min` / `max` reads as empty, with a console warning
63
+ in development, instead of a `RangeError`.
64
+ - **`Collapsible` controlled mode** · `open` + `onOpenChange(open)`. With `open` the product
65
+ holds the state and a click only reports; without it the section keeps its own, as before.
66
+ Both given, `open` wins and `defaultOpen` is ignored (React's contract). A product had rewritten
67
+ the row by hand because it needed to know when a row opened: it fetches on open.
68
+ - **`title` takes a node.** "Accessibility · /pricing" with the path faint; the accessible
69
+ name is `title` and `meta` together.
70
+ - **`note`** · a quiet line that stays visible while the section is closed, between head and
71
+ body, inside the same frame, bound to the head with `aria-describedby`: why a measurement
72
+ did not count, when it last ran.
73
+ - **`unmountOnClose`** · off by default (the body stays in the DOM, `hidden`); on, a heavy body
74
+ is not rendered while closed and `aria-controls` is omitted meanwhile.
75
+ - The caret's turn honours `prefers-reduced-motion`.
76
+ - **`Tag` `swatch`** · the user's own colour as a small mark before the text, `aria-hidden`.
77
+ Hex only (`#rgb`, `#rgba`, `#rrggbb`, `#rrggbbaa`); anything else draws no mark, so user data
78
+ reaches `style` only as a validated value. This is the one **written** exception to Law 3
79
+ ("colour is for deviation and interaction"): the colour is the user's data, an identity mark,
80
+ never a ground or a text colour, never status. The ring around the mark derives from the ink
81
+ (from the page colour inside `look="solid"`), so a near-white or near-black choice stays
82
+ visible in both themes. Written into the physics page and `docs/gerekce/06`, so nobody deletes
83
+ it as a violation or cites it as a precedent. Without `swatch` the output is unchanged.
84
+ - **`ColorMark`** · the mark on its own, on the kit's `tamga-mark` family, for a clickable
85
+ suggestion inside a `Button`.
86
+ - **`ColorSwatches` `noneLabel`** · a "no colour" option; an empty value selects it instead
87
+ of lighting the custom box.
88
+ - **`DangerZone` block** (`tamga-ui/blocks`) · the irreversible actions at the bottom of a
89
+ settings page. A title, an optional description and one or more action rows: an optional
90
+ name, one sentence, a `danger` button. `disabledReason` closes a row and shows the reason as
91
+ **visible text** bound to the button with `aria-describedby` · a disabled button is not a
92
+ reason, and a tooltip on one is invisible to most browsers, the keyboard and touch. `warning`
93
+ takes the kit's `Alert` when a precondition stopped the action; `busy` + `busyLabel` keep the
94
+ button named while it runs. The block never opens a confirm dialog: the click is the caller's,
95
+ the product builds `ConfirmDialog`. Edge and glyph tile in the critical tone, not the whole
96
+ card; the heading and the warning glyph say "danger" without colour. `data-action` sits on the
97
+ button, `data-key` on the row; every row takes `data-*`. A product had hand-written this zone
98
+ three ways on three pages.
99
+ - **`ImageField` upload path** · `onUpload(file) => Promise<string | false>` and
100
+ `onRemove() => Promise<boolean | void>`. Given `onUpload`, the field no longer converts to a
101
+ data URL: it hands the `File` to the caller and carries the states. Waiting: the input and
102
+ every button lock, the button is named by `labels.uploading` / `labels.removing`. Failure: the
103
+ rejected reason (a string, an `Error`, or `{ message }`) under the field in a `role="alert"`
104
+ region, shown as it is, so map a raw network error to your own words first; an empty reason
105
+ shows the new `labels.errorUpload`. The chosen file and its preview stay, "confirm" retries.
106
+ Resolving `onUpload` with `false` keeps the kit silent, for a caller that already handled the
107
+ value or the error (a plan-wall dialog); any other non-string result warns in development,
108
+ because a forgotten `return` must never read as "uploaded". `onRemove` resolving `false` is
109
+ silent the same way. Success: the returned address goes to `onChange`, and a quiet inline
110
+ `role="status"` line (`labels.uploaded` / `labels.removed`). Both live regions are in the DOM
111
+ from the start, only their text changes. `confirm` previews the pick and waits for a click
112
+ (for a logo); the default uploads on pick (for an avatar). `accept` and `maxBytes` are
113
+ checked on both paths before anything else runs, an empty file is refused with
114
+ `errorUnreadable`; `disabled` + `lockedNote` show the reason as visible text wired to the
115
+ buttons with `aria-describedby`. The preview is an object URL, revoked on cancel, on a new
116
+ pick, on success, when `value` changes from outside, and on unmount; a result arriving after
117
+ unmount or after an outside `value` change is dropped, while the caller's own `onChange` with
118
+ the same result (a cache update inside `onUpload`) is not "outside". `maxEdge` is optional in
119
+ the type: not used with `onUpload`, and without either the image is kept at its own size.
120
+ `labels.empty` takes a `ReactNode`. The pre-existing synchronous path (no `onUpload`) renders
121
+ the same markup, locked by test; its only change is that a file outside `accept` is refused
122
+ before `prepareImage` (the input's own list said so already). A product had written this
123
+ field four times.
124
+ - **`LogoTile` `surface`** · `"auto" | "light" | "dark"`, default `auto` (today's behaviour,
125
+ follows the page). `dark` draws the dark theme's ground even on a light page, for a logo drawn
126
+ for dark; `light` the reverse. Ground, edge and initial come as a fixed trio of tokens
127
+ (`--color-logo-ground/edge/ink-light|dark`), identical in both themes, so the tile reads the
128
+ same at night. Measured: initial 6.6:1 on light, 5.9:1 on dark; edge 3.9:1 on dark
129
+ (WCAG 1.4.11). A product that had been wrapping the tile in a local `.dark` class to get this
130
+ can drop the wrapper · note the ground differs by one step: the new dark surface is the dark
131
+ theme's **card** (`#16233a`), the old wrapper with `bg-page` gave the dark **page** (`#101a2c`).
132
+ Not pixel-identical; the card is what a tile sits on.
133
+ - **`Select` `disabled`** · locks the trigger, and looks locked (sunk ground, dashed edge, faint
134
+ ink).
135
+ - `check-item-hooks` also reads `actions` lists, and no longer mistakes `=>` for a closing
136
+ bracket (it was cutting item types short before their `Record<string, unknown>`).
137
+
138
+ ### Changed
139
+
140
+ - The calendar's keyboard model: one tab stop and arrow keys instead of a tab stop on every day.
141
+ A `DatePicker` without `min`/`max` renders the same, but Tab now leaves the grid after one
142
+ stop; the arrows move inside it.
143
+
144
+ ### Fixed
145
+
146
+ - **`DatePicker` `disabled`** left the clear and open buttons live: a locked date could be
147
+ cleared. Both buttons are disabled with the field.
148
+ - **`Button` busy without `busyLabel`** had no accessible name: the label was `invisible` and
149
+ `aria-hidden`. The button's own text now stays in the tree as sr-only, as the docs claimed.
150
+ - **Props tables on the docs site** · `ErrorState` lacked `onRetry` / `retryLabel` (a
151
+ parenthesised union was not read), `Button` lacked `busy` / `busyLabel` and `Input` `leading`
152
+ (an alias body was cut at its first `;`), and tables carried rows from types named inside a
153
+ prop (`AppShell` showed `NavEntry`'s fields). Only top-level intersections expand now, and a
154
+ row type such as `DangerAction` gets its own table.
155
+
8
156
  ## 0.6.0
9
157
 
10
158
  > Sections fold; the one you are in never does.
@@ -0,0 +1,54 @@
1
+ import { type ReactNode } from "react";
2
+ /**
3
+ * TEHLİKELİ BÖLGE · ayarlar sayfasının en altı, geri alınamayan eylemler.
4
+ *
5
+ * Üç ayar sayfası aynı bölgeyi üç ayrı biçimde elle yazıyordu; birinde kapalı
6
+ * düğmenin gerekçesi yalnız `title=` ipucundaydı. Blok onay penceresi AÇMAZ:
7
+ * tık çağırana gider, `ConfirmDialog`u ürün kurar · "adını yazdır" gibi
8
+ * kararlar onda kalır. Gerekçe: docs/gerekce/08-blok-ve-sablon.md
9
+ */
10
+ export type DangerAction = {
11
+ key: string;
12
+ /**
13
+ * The row's title. Optional: when the button's label already names the action ("Delete the
14
+ * customer"), a title would repeat it, and three products had none. TR: Satırın başlığı.
15
+ * İsteğe bağlı: düğmenin metni eylemi zaten adlandırıyorsa ("Müşteriyi sil") başlık onu
16
+ * tekrarlardı, ve üç ürünün hiçbirinde yoktu.
17
+ */
18
+ title?: string;
19
+ /** One sentence saying what will happen. TR: Ne olacağını söyleyen tek cümle. */
20
+ description: string;
21
+ /** The button's text. TR: Düğmenin metni. */
22
+ label: string;
23
+ /** The click is the caller's; open your `ConfirmDialog` here. TR: Tık çağıranın; `ConfirmDialog`unu burada aç. */
24
+ onClick?: () => void;
25
+ /**
26
+ * Given, the row is closed and THIS SENTENCE is shown next to the button and bound to it with
27
+ * `aria-describedby`. A disabled button is not a reason; a tooltip on one is invisible to most
28
+ * browsers, to the keyboard and to touch. TR: Verilirse satır kapalı ve BU CÜMLE düğmenin
29
+ * yanında görünür, `aria-describedby` ile ona bağlı. Kapalı düğme bir gerekçe değildir; üstündeki
30
+ * ipucu çoğu tarayıcıda, klavyede ve dokunmatikte görünmez.
31
+ */
32
+ disabledReason?: string;
33
+ /** The action is running: the button shows a spinner and locks. TR: Eylem sürüyor: düğme spinner gösterir ve kilitlenir. */
34
+ busy?: boolean;
35
+ /** The button's accessible name while busy ("Deleting…"); without it the name would be empty. TR: Sürerken düğmenin erişilebilir adı ("Siliniyor…"); yoksa ad boş kalırdı. */
36
+ busyLabel?: string;
37
+ /**
38
+ * Shown under the row when a precondition stopped the action: the server said "do this first".
39
+ * Put the kit's `Alert` (`state="caution"`) here. TR: Bir önkoşul eylemi durdurduğunda satırın
40
+ * altında · sunucu "önce şunu yap" dedi. Kitin `Alert`ini (`state="caution"`) koy.
41
+ */
42
+ warning?: ReactNode;
43
+ } & Record<string, unknown>;
44
+ export declare function DangerZone({ title, description, actions, className, ...rest }: {
45
+ /** The zone's heading; names the region for assistive tech. TR: Bölgenin başlığı; yardımcı teknoloji için bölgeyi adlandırır. */
46
+ title: string;
47
+ /** One line under the heading: "Nothing here can be undone." TR: Başlığın altında tek satır: "Buradaki işlemler geri alınamaz." */
48
+ description?: string;
49
+ actions: readonly DangerAction[];
50
+ className?: string;
51
+ /** `data-*` hooks pass through. TR: `data-*` kancaları geçiyor. */
52
+ [k: `data-${string}`]: unknown;
53
+ }): import("react").JSX.Element;
54
+ //# sourceMappingURL=danger-zone.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"danger-zone.d.ts","sourceRoot":"","sources":["../../src/blocks/danger-zone.tsx"],"names":[],"mappings":"AAEA,OAAO,EAAS,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAO9C;;;;;;;GAOG;AACH,MAAM,MAAM,YAAY,GAAG;IACzB,GAAG,EAAE,MAAM,CAAC;IACZ;;;;;OAKG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,iFAAiF;IACjF,WAAW,EAAE,MAAM,CAAC;IACpB,6CAA6C;IAC7C,KAAK,EAAE,MAAM,CAAC;IACd,kHAAkH;IAClH,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;IACrB;;;;;;OAMG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,4HAA4H;IAC5H,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,8KAA8K;IAC9K,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;;OAIG;IACH,OAAO,CAAC,EAAE,SAAS,CAAC;CACrB,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AAE5B,wBAAgB,UAAU,CAAC,EACzB,KAAK,EACL,WAAW,EACX,OAAO,EACP,SAAS,EACT,GAAG,IAAI,EACR,EAAE;IACD,iIAAiI;IACjI,KAAK,EAAE,MAAM,CAAC;IACd,mIAAmI;IACnI,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,OAAO,EAAE,SAAS,YAAY,EAAE,CAAC;IACjC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,mEAAmE;IACnE,CAAC,CAAC,EAAE,QAAQ,MAAM,EAAE,GAAG,OAAO,CAAC;CAChC,+BAsDA"}
@@ -0,0 +1,18 @@
1
+ "use client";
2
+ import { createElement as _createElement } from "react";
3
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
4
+ import { useId } from "react";
5
+ import { Button } from "../components/button.js";
6
+ import { Icon } from "../components/icon.js";
7
+ import { Warning } from "../components/icons.js";
8
+ import { cn } from "../lib/cn.js";
9
+ import { dataProps } from "../lib/data-props.js";
10
+ export function DangerZone({ title, description, actions, className, ...rest }) {
11
+ const id = useId();
12
+ return (_jsxs("section", { ...dataProps(rest), className: cn("tamga-danger-zone", className), "aria-labelledby": `${id}-b`, children: [_jsxs("div", { className: "tamga-danger-zone-head", children: [_jsx("span", { className: "tamga-danger-zone-tile", "aria-hidden": "true", children: _jsx(Icon, { icon: Warning, size: "sm", weight: "bold" }) }), _jsxs("div", { className: "min-w-0", children: [_jsx("h3", { id: `${id}-b`, className: "text-subhead font-semibold text-ink", children: title }), description ? _jsx("p", { className: "mt-1 text-small leading-relaxed text-ink-soft", children: description }) : null] })] }), _jsx("ul", { className: "tamga-danger-zone-list", children: actions.map(({ key, title: ad, description: ne, label, onClick, disabledReason, busy, busyLabel, warning, ...satir }, i) => {
13
+ const gerekceId = `${id}-g${i}`;
14
+ return (_createElement("li", { ...dataProps(satir), key: key, "data-key": key, "data-disabled": disabledReason ? "true" : undefined, className: "tamga-danger-zone-row" },
15
+ _jsxs("div", { className: "flex flex-wrap items-start gap-4", children: [_jsxs("div", { className: "min-w-0 flex-1", children: [ad ? _jsx("p", { className: "text-body font-semibold text-ink", children: ad }) : null, _jsx("p", { className: ad ? "text-small leading-relaxed text-ink-soft" : "text-body leading-relaxed text-ink", children: ne }), disabledReason ? (_jsx("p", { id: gerekceId, className: "tamga-danger-zone-reason", "data-reason": true, children: disabledReason })) : null] }), _jsx(Button, { variant: "danger", size: "sm", "data-action": key, onClick: onClick, disabled: Boolean(disabledReason) || busy, busy: busy, busyLabel: busyLabel, "aria-describedby": disabledReason ? gerekceId : undefined, children: label })] }),
16
+ warning ? _jsx("div", { className: "mt-3", children: warning }) : null));
17
+ }) })] }));
18
+ }
@@ -8,4 +8,5 @@
8
8
  export { FilterBar, rangeStart, rangeEnd, type FilterBarLabels, type FilterBarProps, type FilterField, type FilterFieldKind, type FilterGroup, type FilterValues, } from "./filter-bar.js";
9
9
  export { SaveBar } from "./save-bar.js";
10
10
  export { CountRow } from "./count-row.js";
11
+ export { DangerZone, type DangerAction } from "./danger-zone.js";
11
12
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/blocks/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,EACL,SAAS,EACT,UAAU,EACV,QAAQ,EACR,KAAK,eAAe,EACpB,KAAK,cAAc,EACnB,KAAK,WAAW,EAChB,KAAK,eAAe,EACpB,KAAK,WAAW,EAChB,KAAK,YAAY,GAClB,MAAM,iBAAiB,CAAC;AACzB,OAAO,EAAE,OAAO,EAAE,MAAM,eAAe,CAAC;AACxC,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/blocks/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,EACL,SAAS,EACT,UAAU,EACV,QAAQ,EACR,KAAK,eAAe,EACpB,KAAK,cAAc,EACnB,KAAK,WAAW,EAChB,KAAK,eAAe,EACpB,KAAK,WAAW,EAChB,KAAK,YAAY,GAClB,MAAM,iBAAiB,CAAC;AACzB,OAAO,EAAE,OAAO,EAAE,MAAM,eAAe,CAAC;AACxC,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAC1C,OAAO,EAAE,UAAU,EAAE,KAAK,YAAY,EAAE,MAAM,kBAAkB,CAAC"}
@@ -8,3 +8,4 @@
8
8
  export { FilterBar, rangeStart, rangeEnd, } from "./filter-bar.js";
9
9
  export { SaveBar } from "./save-bar.js";
10
10
  export { CountRow } from "./count-row.js";
11
+ export { DangerZone } from "./danger-zone.js";
@@ -9,14 +9,23 @@ export type SwatchOption = {
9
9
  * Brand colour chosen from a few squares, or written by hand.
10
10
  * TR: Birkaç kareden seçilen ya da elle yazılan marka rengi.
11
11
  */
12
- export declare function ColorSwatches({ options, value, onChange, customLabel, custom, className, ...rest }: {
12
+ export declare function ColorSwatches({ options, value, onChange, customLabel, custom, noneLabel, className, ...rest }: {
13
13
  options: readonly SwatchOption[];
14
+ /** The chosen hex; `""` means "no colour" when `noneLabel` is given. TR: Seçili hex; `noneLabel` verilmişse `""` "renk yok" demek. */
14
15
  value: string;
16
+ /** Fires with the hex, or `""` for the "no colour" box. TR: Hex ile ateşlenir; "renk yok" kutusu için `""`. */
15
17
  onChange: (hex: string) => void;
16
18
  /** Accessible name of the "my own colour" control. TR: "Kendi rengim" kontrolünün erişilebilir adı. */
17
19
  customLabel: string;
18
20
  /** Whether a free colour is offered at all. TR: Serbest rengin hiç sunulup sunulmadığı. */
19
21
  custom?: boolean;
22
+ /**
23
+ * Given, a first box means "no colour" and an empty `value` selects it. Without it an empty
24
+ * value lit the custom box, so an optional colour read as a choice. TR: Verilirse ilk kutu
25
+ * "renk yok" demek ve boş `value` onu seçer. Yokken boş değer "kendi rengim" kutusunu
26
+ * yakıyordu, isteğe bağlı bir renk bir seçim gibi okunuyordu.
27
+ */
28
+ noneLabel?: string;
20
29
  className?: string;
21
30
  /** `data-*` hooks pass through. TR: `data-*` kancaları geçiyor. */
22
31
  [k: `data-${string}`]: unknown;
@@ -88,27 +97,104 @@ export declare function RailCards({ value, onChange, labels, className, ...rest
88
97
  * offers reorder arrows with nothing to reorder.
89
98
  * TR: `FileUpload` DEĞİL: o bir GALERİ (çoklu seçim, sıralama okları, "kapak"
90
99
  * rozeti). Ürün fotoğrafları için doğru, tek bir logo için yanlış.
100
+ *
101
+ * Two paths, one field. Without `onUpload` the image is shrunk in the browser
102
+ * and a data URL goes to `onChange`. With `onUpload` the file goes to the
103
+ * caller and the field carries the states: waiting, failure, a quiet
104
+ * confirmation. TR: İki yol, tek alan. `onUpload` yokken görsel tarayıcıda
105
+ * küçültülür, data-URL `onChange`'e gider. `onUpload` verilince dosya çağırana
106
+ * gider; alan bekleme, hata ve sessiz onayı taşır.
107
+ *
108
+ * Gerekçe: docs/gerekce/01-form-ve-girdi.md
91
109
  */
92
- export declare function ImageField({ value, onChange, maxEdge, labels, preview, extra, className, ...rest }: {
110
+ export declare function ImageField({ value, onChange, maxEdge, labels, preview, extra, onUpload, onRemove, confirm, accept, maxBytes, disabled, lockedNote, className, ...rest }: {
93
111
  value: string | null;
94
112
  onChange: (next: string | null) => void;
95
- /** Longest edge of the stored image, in pixels. TR: Saklanan görselin en uzun kenarı, piksel. */
96
- maxEdge: number;
113
+ /**
114
+ * Longest edge of the stored image, in pixels; the browser path shrinks to it, so give it
115
+ * there. Not used with `onUpload`: the server decides. TR: Saklanan görselin en uzun kenarı,
116
+ * piksel; tarayıcı yolu ona küçültür, orada ver. `onUpload` ile kullanılmaz: sunucu karar verir.
117
+ */
118
+ maxEdge?: number;
97
119
  labels: {
98
120
  /** "Logo", "Mark": used inside the buttons. TR: "Logo", "Amblem": düğmelerin içinde geçiyor. */
99
121
  name: string;
100
122
  upload: string;
101
123
  replace: string;
102
124
  remove: string;
103
- empty: string;
125
+ /** The empty state's content: a sentence, or a node. TR: Boş durumun içeriği: bir cümle ya da bir düğüm. */
126
+ empty: ReactNode;
104
127
  errorType: string;
105
128
  errorSize: string;
106
129
  errorUnreadable: string;
130
+ /**
131
+ * Shown when `onUpload` or `onRemove` rejects without a message (an empty string, `undefined`);
132
+ * falls back to `errorUnreadable`. TR: `onUpload` ya da `onRemove` mesajsız reddedince (boş
133
+ * metin, `undefined`); yoksa `errorUnreadable`.
134
+ */
135
+ errorUpload?: string;
136
+ /** The button's name while `onUpload` runs; falls back to `upload`. TR: `onUpload` sürerken düğmenin adı; yoksa `upload`. */
137
+ uploading?: string;
138
+ /** The button's name while `onRemove` runs; falls back to `remove`. TR: `onRemove` sürerken düğmenin adı; yoksa `remove`. */
139
+ removing?: string;
140
+ /** The quiet line after a successful upload; omitted, nothing is said. TR: Başarılı yüklemeden sonraki sessiz satır; yoksa susar. */
141
+ uploaded?: string;
142
+ /** The quiet line after a successful removal. TR: Başarılı kaldırmadan sonraki sessiz satır. */
143
+ removed?: string;
144
+ /** The button that sends a previewed pick; falls back to `upload`. TR: Önizlenen seçimi gönderen düğme; yoksa `upload`. */
145
+ confirm?: string;
146
+ /** The button that drops a previewed pick; falls back to `remove`. TR: Önizlenen seçimi bırakan düğme; yoksa `remove`. */
147
+ cancel?: string;
107
148
  };
108
149
  /** The caller draws the preview: a logo is wide, a mark is square. TR: Önizlemeyi çağıran çiziyor: logo geniş, amblem kare. */
109
150
  preview: (src: string) => ReactNode;
110
151
  /** An extra control beside the buttons. TR: Düğmelerin yanına giren ek kontrol. */
111
152
  extra?: ReactNode;
153
+ /**
154
+ * Hands the chosen file to the caller instead of converting it. Resolve with the stored
155
+ * address: it goes to `onChange` and `labels.uploaded` is announced. Resolve with `false` when
156
+ * you handled the result yourself (a plan-wall dialog, your own `onChange`): the kit stays
157
+ * silent. Anything else is a bug and warns in development, because a forgotten `return` must
158
+ * not read as "uploaded". Reject with a string, an `Error` or `{ message }`: that text is shown
159
+ * under the field AS IT IS, so map a raw network error to your own words before rejecting; an
160
+ * empty reason shows `labels.errorUpload`. The file and its preview stay, "confirm" retries. A
161
+ * result that arrives after `value` changed from outside, or after unmount, is dropped. TR:
162
+ * Seçilen dosyayı dönüştürmek yerine çağırana verir. Saklanan adresle çözülür: `onChange`'e
163
+ * gider, `labels.uploaded` duyurulur. Sonucu kendin ele aldıysan `false` ile çöz: kit susar.
164
+ * Başka bir değer geliştirmede uyarır; unutulan bir `return` "yüklendi" sayılmaz. Metin, `Error`
165
+ * ya da `{ message }` ile reddedilirse metin alanın altında OLDUĞU GİBİ gösterilir, ağ hatasını
166
+ * reddetmeden önce kendi sözüne çevir; boş sebep `labels.errorUpload` gösterir. Dosya ve
167
+ * önizleme kalır, "confirm" yeniden dener. `value` dışarıdan değiştikten ya da bileşen
168
+ * kalktıktan sonra gelen sonuç atılır.
169
+ */
170
+ onUpload?: (file: File) => Promise<string | false>;
171
+ /**
172
+ * Awaited before `onChange(null)`; `false` keeps the kit silent (you handled it); a rejection
173
+ * shows under the field like `onUpload`'s. TR: `onChange(null)`'dan önce beklenir; `false` kiti
174
+ * susturur (sen ele aldın); red `onUpload`'daki gibi alanın altında.
175
+ */
176
+ onRemove?: () => Promise<boolean | void>;
177
+ /**
178
+ * Preview first, send on a click: for a logo, a chance to notice the wrong file. Off, the pick
179
+ * uploads at once (an avatar). TR: Önce önizle, tıklayınca gönder: logo için yanlış dosyayı fark
180
+ * etme şansı. Kapalıyken seçim hemen gider (avatar).
181
+ */
182
+ confirm?: boolean;
183
+ /**
184
+ * The input's accept list; the pick is checked against it on both paths, before `prepareImage`
185
+ * or `onUpload`. TR: Girdinin accept listesi; seçim iki yolda da `prepareImage` ya da
186
+ * `onUpload`'dan önce buna göre denetlenir.
187
+ */
188
+ accept?: string;
189
+ /** Refused on both paths with `errorSize`; 8 MB by default. TR: İki yolda da `errorSize` ile reddedilir; varsayılan 8 MB. */
190
+ maxBytes?: number;
191
+ disabled?: boolean;
192
+ /**
193
+ * Why the field is locked, as visible text under it and `aria-describedby` on the buttons; a
194
+ * tooltip hides from touch and keyboards. TR: Alanın neden kilitli olduğu, altında görünür
195
+ * metin ve düğmelerde `aria-describedby`; ipucu dokunmatikten ve klavyeden saklanır.
196
+ */
197
+ lockedNote?: string;
112
198
  className?: string;
113
199
  /** `data-*` hooks pass through. TR: `data-*` kancaları geçiyor. */
114
200
  [k: `data-${string}`]: unknown;
@@ -1 +1 @@
1
- {"version":3,"file":"appearance.d.ts","sourceRoot":"","sources":["../../src/components/appearance.tsx"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"appearance.d.ts","sourceRoot":"","sources":["../../src/components/appearance.tsx"],"names":[],"mappings":"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"}
@@ -1,22 +1,24 @@
1
1
  "use client";
2
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
- import { useId, useRef, useState } from "react";
2
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useEffect, useId, useRef, useState } from "react";
4
4
  import { cn } from "../lib/cn.js";
5
5
  import { makePalette } from "../lib/palette.js";
6
6
  import { dataProps } from "../lib/data-props.js";
7
- import { IMAGE_ACCEPT, prepareImage } from "../lib/image.js";
7
+ import { IMAGE_ACCEPT, RAW_LIMIT, prepareImage } from "../lib/image.js";
8
+ import { uyar } from "../lib/uyar.js";
8
9
  import { Alert } from "./primitives.js";
9
10
  import { Button } from "./button.js";
10
11
  import { Icon } from "./icon.js";
11
- import { Check, Eyedropper } from "./icons.js";
12
+ import { Check, Eyedropper, Close } from "./icons.js";
12
13
  /**
13
14
  * Brand colour chosen from a few squares, or written by hand.
14
15
  * TR: Birkaç kareden seçilen ya da elle yazılan marka rengi.
15
16
  */
16
- export function ColorSwatches({ options, value, onChange, customLabel, custom = true, className, ...rest }) {
17
+ export function ColorSwatches({ options, value, onChange, customLabel, custom = true, noneLabel, className, ...rest }) {
17
18
  const secili = (hex) => hex.toLowerCase() === value.toLowerCase();
18
- const hazirMi = options.some((o) => secili(o.hex));
19
- return (_jsxs("span", { ...dataProps(rest), className: cn("flex flex-wrap items-center gap-3", className), children: [options.map(({ hex, label: etiket, ...rest }) => (_jsx("button", { ...rest, type: "button", className: "tamga-swatch", style: { background: hex, color: mürekkep(hex) }, "data-selected": secili(hex), "aria-pressed": secili(hex), "aria-label": etiket, title: etiket, onClick: () => onChange(hex), children: secili(hex) ? _jsx(Icon, { icon: Check, size: "base", weight: "bold" }) : null }, hex))), custom ? (_jsxs("label", { className: "tamga-swatch tamga-swatch-custom", "data-selected": !hazirMi, title: customLabel, style: hazirMi
19
+ const yokSecili = noneLabel !== undefined && value === "";
20
+ const hazirMi = yokSecili || options.some((o) => secili(o.hex));
21
+ return (_jsxs("span", { ...dataProps(rest), className: cn("flex flex-wrap items-center gap-3", className), children: [noneLabel !== undefined ? (_jsx("button", { type: "button", className: "tamga-swatch tamga-swatch-none", "data-selected": yokSecili, "aria-pressed": yokSecili, "aria-label": noneLabel, title: noneLabel, onClick: () => onChange(""), children: yokSecili ? _jsx(Icon, { icon: Check, size: "base", weight: "bold" }) : _jsx(Icon, { icon: Close, size: "base", weight: "bold" }) })) : null, options.map(({ hex, label: etiket, ...rest }) => (_jsx("button", { ...rest, type: "button", className: "tamga-swatch", style: { background: hex, color: mürekkep(hex) }, "data-selected": secili(hex), "aria-pressed": secili(hex), "aria-label": etiket, title: etiket, onClick: () => onChange(hex), children: secili(hex) ? _jsx(Icon, { icon: Check, size: "base", weight: "bold" }) : null }, hex))), custom ? (_jsxs("label", { className: "tamga-swatch tamga-swatch-custom", "data-selected": !hazirMi, title: customLabel, style: hazirMi
20
22
  ? /* Hiçbir hazır renk seçili değilken kutu o rengi giyiyor;
21
23
  seçiliyken damalı zemin "burada henüz bir renk yok" diyor. */
22
24
  { background: DAMA }
@@ -117,29 +119,173 @@ export function RailCards({ value, onChange, labels, className, ...rest }) {
117
119
  * offers reorder arrows with nothing to reorder.
118
120
  * TR: `FileUpload` DEĞİL: o bir GALERİ (çoklu seçim, sıralama okları, "kapak"
119
121
  * rozeti). Ürün fotoğrafları için doğru, tek bir logo için yanlış.
122
+ *
123
+ * Two paths, one field. Without `onUpload` the image is shrunk in the browser
124
+ * and a data URL goes to `onChange`. With `onUpload` the file goes to the
125
+ * caller and the field carries the states: waiting, failure, a quiet
126
+ * confirmation. TR: İki yol, tek alan. `onUpload` yokken görsel tarayıcıda
127
+ * küçültülür, data-URL `onChange`'e gider. `onUpload` verilince dosya çağırana
128
+ * gider; alan bekleme, hata ve sessiz onayı taşır.
129
+ *
130
+ * Gerekçe: docs/gerekce/01-form-ve-girdi.md
120
131
  */
121
- export function ImageField({ value, onChange, maxEdge, labels, preview, extra, className, ...rest }) {
132
+ export function ImageField({ value, onChange, maxEdge, labels, preview, extra, onUpload, onRemove, confirm = false, accept = IMAGE_ACCEPT, maxBytes = RAW_LIMIT, disabled = false, lockedNote, className, ...rest }) {
122
133
  const id = useId();
123
134
  const girdi = useRef(null);
124
135
  const [hata, setHata] = useState(null);
136
+ const [bekleyen, setBekleyen] = useState(null);
137
+ const [mesgul, setMesgul] = useState(null);
138
+ const [onay, setOnay] = useState(null);
139
+ /* Adres ref'te de: kalkışta son adresi bırakmak için state okunamıyor. İstek
140
+ sayacı, "canlı" bayrağı ve son değer bayat cevabı ayırıyor: kalkıştan ya da
141
+ dış değer değişiminden sonra gelen sonuç ne onChange'e gider ne ekrana. */
142
+ const adres = useRef(null);
143
+ const istek = useRef(0);
144
+ const canli = useRef(true);
145
+ const sonDeger = useRef(value);
146
+ sonDeger.current = value;
147
+ function birak() {
148
+ if (adres.current)
149
+ URL.revokeObjectURL(adres.current);
150
+ adres.current = null;
151
+ setBekleyen(null);
152
+ }
153
+ useEffect(() => {
154
+ canli.current = true;
155
+ return () => {
156
+ canli.current = false;
157
+ if (adres.current)
158
+ URL.revokeObjectURL(adres.current);
159
+ adres.current = null;
160
+ };
161
+ }, []);
162
+ /* Değer dışarıdan geldiyse bekleyen önizleme bayat kaldı. */
163
+ useEffect(() => {
164
+ if (value && adres.current)
165
+ birak();
166
+ }, [value]);
167
+ const kilit = disabled || mesgul !== null;
168
+ /* Dış değer değiştiyse sonuç bayat · ama çağıranın KENDİ güncellemesi değil:
169
+ onUpload içinde önbelleği sonuçla, onRemove içinde null ile güncelleyen bir
170
+ çağıran "yüklendi"yi yine duymalı. */
171
+ const bayat = (benim, baslangic, beklenen) => !canli.current ||
172
+ istek.current !== benim ||
173
+ (sonDeger.current !== baslangic && (beklenen === undefined || sonDeger.current !== beklenen));
174
+ function sifirla() {
175
+ /* GİRDİ SIFIRLANIYOR: aynı dosya ikinci kez seçilince `change` hiç
176
+ çıkmıyor, çünkü değer değişmiyor. Kullanıcı için bu "ikinci denemede
177
+ çalışmıyor" demek. */
178
+ if (girdi.current)
179
+ girdi.current.value = "";
180
+ }
125
181
  async function al(file) {
126
- if (!file)
182
+ if (!file || kilit)
183
+ return;
184
+ sifirla();
185
+ setHata(null);
186
+ setOnay(null);
187
+ if (!kabul(file, accept))
188
+ return setHata(labels.errorType);
189
+ if (file.size === 0)
190
+ return setHata(labels.errorUnreadable);
191
+ if (file.size > maxBytes)
192
+ return setHata(labels.errorSize);
193
+ if (!onUpload) {
194
+ try {
195
+ onChange(await prepareImage(file, maxEdge ?? Infinity));
196
+ }
197
+ catch (e) {
198
+ const kod = e instanceof Error ? e.message : "unreadable";
199
+ setHata(kod === "type" ? labels.errorType : kod === "size" ? labels.errorSize : labels.errorUnreadable);
200
+ }
201
+ return;
202
+ }
203
+ birak();
204
+ const url = URL.createObjectURL(file);
205
+ adres.current = url;
206
+ setBekleyen({ file, url });
207
+ if (!confirm)
208
+ await gonder(file);
209
+ }
210
+ async function gonder(file) {
211
+ if (!onUpload || mesgul)
212
+ return;
213
+ const benim = ++istek.current;
214
+ const baslangic = sonDeger.current;
215
+ setHata(null);
216
+ setOnay(null);
217
+ setMesgul("upload");
218
+ try {
219
+ const sonuc = await onUpload(file);
220
+ if (bayat(benim, baslangic, typeof sonuc === "string" ? sonuc : undefined))
221
+ return;
222
+ if (typeof sonuc === "string") {
223
+ birak();
224
+ onChange(sonuc);
225
+ setOnay(labels.uploaded ?? null);
226
+ }
227
+ else if (sonuc !== false) {
228
+ uyar("ImageField: onUpload resolved with neither an address nor `false`; nothing was announced. Return the stored address, or `false` when you handled the result yourself.");
229
+ }
230
+ }
231
+ catch (e) {
232
+ if (bayat(benim, baslangic, undefined))
233
+ return;
234
+ setHata(metin(e, labels));
235
+ }
236
+ finally {
237
+ if (canli.current)
238
+ setMesgul(null);
239
+ }
240
+ }
241
+ async function kaldir() {
242
+ if (kilit)
127
243
  return;
128
244
  setHata(null);
245
+ setOnay(null);
246
+ if (!onRemove)
247
+ return onChange(null);
248
+ const benim = ++istek.current;
249
+ const baslangic = sonDeger.current;
250
+ setMesgul("remove");
129
251
  try {
130
- onChange(await prepareImage(file, maxEdge));
252
+ const sonuc = await onRemove();
253
+ if (bayat(benim, baslangic, null) || sonuc === false)
254
+ return;
255
+ onChange(null);
256
+ setOnay(labels.removed ?? null);
131
257
  }
132
258
  catch (e) {
133
- const kod = e instanceof Error ? e.message : "unreadable";
134
- setHata(kod === "type" ? labels.errorType : kod === "size" ? labels.errorSize : labels.errorUnreadable);
259
+ if (bayat(benim, baslangic, undefined))
260
+ return;
261
+ setHata(metin(e, labels));
135
262
  }
136
263
  finally {
137
- /* GİRDİ SIFIRLANIYOR: aynı dosya ikinci kez seçilince `change` hiç
138
- çıkmıyor, çünkü değer değişmiyor. Kullanıcı için bu "ikinci denemede
139
- çalışmıyor" demek. */
140
- if (girdi.current)
141
- girdi.current.value = "";
264
+ if (canli.current)
265
+ setMesgul(null);
142
266
  }
143
267
  }
144
- return (_jsxs("div", { ...dataProps(rest), className: cn("flex flex-col gap-3", className), children: [_jsx("input", { ref: girdi, id: id, type: "file", accept: IMAGE_ACCEPT, className: "sr-only", onChange: (e) => al(e.target.files?.[0]) }), _jsxs("div", { className: "flex flex-wrap items-center gap-4", children: [_jsx("span", { className: "tamga-image-field", "data-empty": value ? undefined : "true", children: value ? preview(value) : labels.empty }), _jsxs("span", { className: "flex flex-wrap items-center gap-2", children: [_jsx(Button, { type: "button", onClick: () => girdi.current?.click(), children: value ? labels.replace : labels.upload }), extra, value ? (_jsx(Button, { type: "button", variant: "ghost", onClick: () => onChange(null), children: labels.remove })) : null] })] }), hata ? _jsx(Alert, { state: "danger", title: hata }) : null] }));
268
+ const gosterilen = bekleyen?.url ?? value;
269
+ const asenkron = Boolean(onUpload || onRemove);
270
+ const notKimligi = `${id}-kilit`;
271
+ const tarif = disabled && lockedNote ? notKimligi : undefined;
272
+ return (_jsxs("div", { ...dataProps(rest), className: cn("flex flex-col gap-3", className), children: [_jsx("input", { ref: girdi, id: id, type: "file", accept: accept, className: "sr-only", disabled: kilit || undefined, onChange: (e) => al(e.target.files?.[0]) }), _jsxs("div", { className: "flex flex-wrap items-center gap-4", children: [_jsx("span", { className: "tamga-image-field", "data-empty": gosterilen ? undefined : "true", children: gosterilen ? preview(gosterilen) : labels.empty }), _jsxs("span", { className: "flex flex-wrap items-center gap-2", children: [bekleyen ? (_jsxs(_Fragment, { children: [_jsx(Button, { type: "button", busy: mesgul === "upload", busyLabel: labels.uploading ?? labels.upload, disabled: kilit || undefined, "aria-describedby": tarif, onClick: () => gonder(bekleyen.file), children: labels.confirm ?? labels.upload }), _jsx(Button, { type: "button", variant: "ghost", disabled: kilit || undefined, onClick: birak, children: labels.cancel ?? labels.remove })] })) : (_jsx(Button, { type: "button", busy: mesgul === "upload", busyLabel: labels.uploading ?? labels.upload, disabled: kilit || undefined, "aria-describedby": tarif, onClick: () => girdi.current?.click(), children: value ? labels.replace : labels.upload })), extra, value && !bekleyen ? (_jsx(Button, { type: "button", variant: "ghost", busy: mesgul === "remove", busyLabel: labels.removing ?? labels.remove, disabled: kilit || undefined, "aria-describedby": tarif, onClick: kaldir, children: labels.remove })) : null] })] }), lockedNote && disabled ? (_jsx("p", { id: notKimligi, className: "text-small text-ink-faint", children: lockedNote })) : null, asenkron ? (_jsx("p", { role: "status", className: cn("text-small text-ink-faint", !onay && "sr-only"), children: onay })) : null, asenkron ? (_jsx("div", { role: "alert", className: cn(!hata && "sr-only"), children: hata ? _jsx(Alert, { state: "danger", title: hata }) : null })) : hata ? (_jsx(Alert, { state: "danger", title: hata })) : null] }));
273
+ }
274
+ /** `accept` listesine karşı ön denetim: MIME, `image/*` ve `.png` biçimleri. Boş liste her şeyi geçirir. */
275
+ function kabul(file, accept) {
276
+ const liste = accept.split(",").map((s) => s.trim().toLowerCase()).filter(Boolean);
277
+ if (liste.length === 0)
278
+ return true;
279
+ const tur = file.type.toLowerCase();
280
+ const ad = file.name.toLowerCase();
281
+ return liste.some((k) => k.startsWith(".") ? ad.endsWith(k) : k.endsWith("/*") ? tur.startsWith(k.slice(0, -1)) : tur === k);
282
+ }
283
+ /** Çağıranın reddi: metin, `Error` ya da `{ message }` olduğu gibi; boşsa kitin genel sözü. */
284
+ function metin(e, labels) {
285
+ const yedek = labels.errorUpload ?? labels.errorUnreadable;
286
+ if (typeof e === "string")
287
+ return e || yedek;
288
+ if (e && typeof e === "object" && "message" in e && typeof e.message === "string" && e.message)
289
+ return e.message;
290
+ return yedek;
145
291
  }