base — Sdílené SCSS proměnné, mixiny a JS utility — fundament pro všechny komponenty

resources/sass/components/base/_variables.scss
// ─── CzG UI Registry — base/variables ───────────────────────────────────────
// Konvence:
//   - Canonical názvy: `$cgui-*` (oficiální API)
//   - Legacy aliases: `$color__*`, `$small`, `$radius`... (zachováno pro kompatibilitu
//     se stávajícími komponentami; nové komponenty MAJÍ POUŽÍVAT `$cgui-*`)
//   - Project override: nastav `$cgui-color-primary: #FF0000;` PŘED `@import`
//   - Px only (žádné rem/em — user může mít zoom 200% a layout se rozjede)

// ─── 0. Theme switch flag (registry preview compile) ────────────────────────
$cgui-dark-mode: false !default;


// ─── 1. Brand colors ────────────────────────────────────────────────────────
@if $cgui-dark-mode {
	$cgui-color-primary:        #5577FF !default;
	$cgui-color-secondary:      #94A3B8 !default;
	$cgui-color-brand:          #5577FF !default;
	$cgui-color-commerce:       #34D399 !default;
	$cgui-color-discount:       #F87171 !default;
	$cgui-color-yellow:         #FDE047 !default;
	$cgui-color-red:            #F87171 !default;
	$cgui-color-green:          #34D399 !default;
	$cgui-color-blue:           #5577FF !default;
	$cgui-color-access:         #F1F5F9 !default;

	$cgui-color-text:           #F1F5F9 !default;
	$cgui-color-text-muted:     #94A3B8 !default;
	$cgui-color-text-gray:      #94A3B8 !default;
	$cgui-color-text-dark:      #FFFFFF !default;
	$cgui-color-text-light:     #CBD5E1 !default;

	$cgui-color-bg:             #0F172A !default;
	$cgui-color-bg-light:       #1E293B !default;
	$cgui-color-bg-gray:        #334155 !default;
	$cgui-color-bg-gray-light:  #1E293B !default;
	$cgui-color-bg-gray-dark:   #475569 !default;
	$cgui-color-bg-form:        #1E293B !default;
	$cgui-color-bg-selected:    #14532D !default;
	$cgui-color-bg-hover:       #1E293B !default;
	$cgui-color-bg-dark:        #020617 !default;

	$cgui-color-border:         #334155 !default;
	$cgui-color-border-light:   #1E293B !default;

	$cgui-color-error:          #F87171 !default;
	$cgui-color-success:        #34D399 !default;
	$cgui-color-warning:        #FBBF24 !default;
} @else {
	$cgui-color-primary:        #003CFF !default;
	$cgui-color-secondary:      #000000 !default;
	$cgui-color-brand:          #003CFF !default;
	$cgui-color-commerce:       #00880B !default;
	$cgui-color-discount:       #FF0057 !default;
	$cgui-color-yellow:         #FFEE00 !default;
	$cgui-color-red:            #FF5656 !default;
	$cgui-color-green:          #18B21D !default;
	$cgui-color-blue:           #003CFF !default;
	$cgui-color-access:         #000000 !default;

	$cgui-color-text:           #1F2937 !default;
	$cgui-color-text-muted:     #6B7280 !default;
	$cgui-color-text-gray:      #848484 !default;
	$cgui-color-text-dark:      #000000 !default;
	$cgui-color-text-light:     #6B7280 !default;

	$cgui-color-bg:             #FFFFFF !default;
	$cgui-color-bg-light:       #FFFFFF !default;
	$cgui-color-bg-gray:        #CCCCCC !default;
	$cgui-color-bg-gray-light:  #F1F1F1 !default;
	$cgui-color-bg-gray-dark:   #DFDFDF !default;
	$cgui-color-bg-form:        #F3F3F3 !default;
	$cgui-color-bg-selected:    #CFFFE4 !default;
	$cgui-color-bg-hover:       #F3F4F6 !default;
	$cgui-color-bg-dark:        #000000 !default;

	$cgui-color-border:         #DFDFDF !default;
	$cgui-color-border-light:   #F3F3F3 !default;

	$cgui-color-error:          #EF4444 !default;
	$cgui-color-success:        #00880B !default;
	$cgui-color-warning:        #F59E0B !default;
}


// ─── 2. Spacing scale (multiples of base — px only) ─────────────────────────
$cgui-space-base: 8px !default;

$cgui-space-0:  0 !default;
$cgui-space-1:  4px  !default;
$cgui-space-2:  8px  !default;
$cgui-space-3:  12px !default;
$cgui-space-4:  16px !default;
$cgui-space-5:  24px !default;
$cgui-space-6:  32px !default;
$cgui-space-7:  48px !default;
$cgui-space-8:  64px !default;
$cgui-space-9:  96px !default;


// ─── 3. Typography (px — žádné rem/em pro nezávislost na zoomu) ─────────────
$cgui-font-family-base:    -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif !default;
$cgui-font-family-heading: $cgui-font-family-base !default;
$cgui-font-family-mono:    "SF Mono", Menlo, Consolas, monospace !default;

$cgui-font-size-xs:  12px !default;
$cgui-font-size-sm:  14px !default;
$cgui-font-size-md:  16px !default;
$cgui-font-size-lg:  18px !default;
$cgui-font-size-xl:  24px !default;
$cgui-font-size-xxl: 32px !default;

$cgui-font-weight-regular: 400 !default;
$cgui-font-weight-medium:  500 !default;
$cgui-font-weight-bold:    700 !default;

$cgui-line-height-tight:  1.2 !default;
$cgui-line-height-small:  1.35 !default;
$cgui-line-height-normal: 1.5 !default;
$cgui-line-height-loose:  1.75 !default;


// ─── 4. Layout ──────────────────────────────────────────────────────────────
$cgui-content:           1400px !default;
$cgui-content-medium:     970px !default;
$cgui-content-small:      768px !default;
$cgui-content-padding:     16px !default;


// ─── 5. Borders + radius ────────────────────────────────────────────────────
$cgui-radius-sm:    3px !default;
$cgui-radius:       6px !default;
$cgui-radius-lg:   12px !default;
$cgui-radius-pill: 999px !default;

$cgui-border-width: 1px !default;


// ─── 6. Shadows ─────────────────────────────────────────────────────────────
@if $cgui-dark-mode {
	$cgui-shadow-sm:   0 1px 2px rgba(0, 0, 0, 0.4) !default;
	$cgui-shadow-main: 0 4px 12px rgba(0, 0, 0, 0.5) !default;
	$cgui-shadow-lg:   0 8px 24px rgba(0, 0, 0, 0.6) !default;
} @else {
	$cgui-shadow-sm:   0 1px 2px rgba(0, 0, 0, 0.06) !default;
	$cgui-shadow-main: 0 4px 12px rgba(0, 0, 0, 0.08) !default;
	$cgui-shadow-lg:   0 8px 24px rgba(0, 0, 0, 0.12) !default;
}


// ─── 7. Transitions / easing ────────────────────────────────────────────────
$cgui-ease:        all 0.15s ease !default;
$cgui-ease-in:     all 0.2s  ease-in !default;
$cgui-ease-out:    all 0.2s  ease-out !default;
$cgui-ease-in-out: all 0.25s ease-in-out !default;


// ─── 8. Breakpoints ─────────────────────────────────────────────────────────
$cgui-bp-min:     320px !default;
$cgui-bp-tiny:    480px !default;
$cgui-bp-small:   640px !default;
$cgui-bp-medium:  960px !default;
$cgui-bp-large:   1280px !default;
$cgui-bp-xlarge:  1700px !default;
$cgui-bp-max:     1920px !default;


// ─── 9. Z-index layers ──────────────────────────────────────────────────────
$cgui-z-base:        1 !default;
$cgui-z-dropdown:  100 !default;
$cgui-z-sticky:    500 !default;
$cgui-z-overlay:   900 !default;
$cgui-z-modal:    1000 !default;
$cgui-z-toast:    2000 !default;
$cgui-z-tooltip:  3000 !default;


// (žádné legacy aliasy — komponenty používají VÝHRADNĚ `$cgui-*` proměnné výše)
resources/sass/components/base/_functions.scss
// ─── Strip unit from value ──────────────────────────────────────────────────
// Use: cgui-strip-unit(16px) -> 16
@function cgui-strip-unit($value) {
	@if type-of($value) == 'number' and not unitless($value) {
		@return $value / ($value * 0 + 1);
	}
	@return $value;
}


// ─── Spacing helper ─────────────────────────────────────────────────────────
// Use: spacing(2) -> 16px (= 2 × $cgui-space-base)
@function cgui-spacing($n) {
	@return $cgui-space-base * $n;
}


// ─── Semantic z-index lookup ────────────────────────────────────────────────
// Use: z(modal) -> 1000
@function cgui-z($layer) {
	$layers: (
		base:     $cgui-z-base,
		dropdown: $cgui-z-dropdown,
		sticky:   $cgui-z-sticky,
		overlay:  $cgui-z-overlay,
		modal:    $cgui-z-modal,
		toast:    $cgui-z-toast,
		tooltip:  $cgui-z-tooltip,
	);

	@if not map-has-key($layers, $layer) {
		@warn "z(#{$layer}) — unknown layer. Available: #{map-keys($layers)}";
		@return null;
	}
	@return map-get($layers, $layer);
}


// ─── Color shade helpers (build-time SCSS, hex/rgb only) ────────────────────
// Use: shade($cgui-color-primary, 10%) — ztmaví o 10%
//      tint($cgui-color-primary, 10%)  — světlejší o 10%
@function cgui-shade($color, $amount) {
	@return mix(black, $color, $amount);
}

@function cgui-tint($color, $amount) {
	@return mix(white, $color, $amount);
}


// ─── Encode color for SVG data-URI ──────────────────────────────────────────
// Use: background: url("data:...,#{encode-color($cgui-color-primary)}");
@function cgui-encode-color($color) {
	@return '%23' + str-slice(inspect($color), 2);
}
resources/sass/components/base/_mixins.scss
// ─── Responsive ─────────────────────────────────────────────────────────────
// Use: @include media(small) { ... } nebo @include media(900px, down) { ... }
@mixin cgui-media($breakpoint, $direction: up) {
	$bp: $breakpoint;

	@if $breakpoint == tiny    { $bp: $cgui-bp-tiny; }
	@else if $breakpoint == small   { $bp: $cgui-bp-small; }
	@else if $breakpoint == medium  { $bp: $cgui-bp-medium; }
	@else if $breakpoint == large   { $bp: $cgui-bp-large; }
	@else if $breakpoint == xlarge  { $bp: $cgui-bp-xlarge; }
	@else if $breakpoint == max     { $bp: $cgui-bp-max; }

	@if $direction == up {
		@media (min-width: $bp) { @content; }
	} @else if $direction == down {
		@media (max-width: ($bp - 1px)) { @content; }
	} @else if $direction == only {
		@media (min-width: $bp) and (max-width: ($bp + 320px - 1px)) { @content; }
	}
}


// ─── Hover (skip touch) ─────────────────────────────────────────────────────
// Use: @include hover() { background: red; }
@mixin cgui-hover() {
	@media (hover: hover) {
		&:hover, &:focus-visible { @content; }
	}
}


// ─── Focus visible (a11y) ───────────────────────────────────────────────────
// Use: a { @include focus-visible(); }
@mixin cgui-focus-visible($color: $cgui-color-primary, $offset: 2px) {
	&:focus { outline: none; }
	&:focus-visible {
		outline: 2px solid $color;
		outline-offset: $offset;
	}
}


// ─── Visually hidden (sr-only) ──────────────────────────────────────────────
// Skryje vizuálně, ale zůstává pro screen readers.
@mixin cgui-visually-hidden() {
	position: absolute;
	width: 1px;
	height: 1px;
	padding: 0;
	margin: -1px;
	overflow: hidden;
	clip: rect(0, 0, 0, 0);
	white-space: nowrap;
	border: 0;
}


// ─── Truncate text ──────────────────────────────────────────────────────────
// Use: @include truncate();        — single line
//      @include truncate(3);       — 3 řádky
@mixin cgui-truncate($lines: 1) {
	@if $lines == 1 {
		overflow: hidden;
		text-overflow: ellipsis;
		white-space: nowrap;
	} @else {
		display: -webkit-box;
		-webkit-line-clamp: $lines;
		-webkit-box-orient: vertical;
		overflow: hidden;
	}
}


// ─── Pseudo element shortcut ────────────────────────────────────────────────
// Use: &::before { @include pseudo(); ... }
@mixin cgui-pseudo($display: block, $position: absolute, $cgui-content: '') {
	content: $cgui-content;
	display: $display;
	position: $position;
}


// ─── Container with max-width ───────────────────────────────────────────────
@mixin cgui-container($cgui-bp-max-width: $cgui-content, $padding: $cgui-content-padding) {
	width: 100%;
	max-width: $cgui-bp-max-width;
	margin-left: auto;
	margin-right: auto;
	padding-left: $padding;
	padding-right: $padding;
	box-sizing: border-box;
}


// ─── Reset list (no bullets, no padding) ────────────────────────────────────
@mixin cgui-reset-list() {
	list-style: none;
	margin: 0;
	padding: 0;
}


// ─── Reset button (strip native styles) ─────────────────────────────────────
@mixin cgui-reset-button() {
	background: none;
	border: none;
	padding: 0;
	margin: 0;
	font: inherit;
	color: inherit;
	cursor: pointer;
	text-align: inherit;
}


// ─── Aspect ratio (fallback for old browsers) ───────────────────────────────
// Modern: aspect-ratio property je dnes baseline
@mixin cgui-aspect-ratio($w, $h) {
	aspect-ratio: $w / $h;
}


// ─── Scrollbar styling ──────────────────────────────────────────────────────
@mixin cgui-scrollbar-thin($thumb: $cgui-color-border, $track: transparent) {
	scrollbar-width: thin;
	scrollbar-color: $thumb $track;

	&::-webkit-scrollbar { width: 8px; height: 8px; }
	&::-webkit-scrollbar-track { background: $track; }
	&::-webkit-scrollbar-thumb {
		background: $thumb;
		border-radius: 4px;

		&:hover { background: $cgui-color-border-light; }
	}
}


// ─── Stretched link (whole card clickable) ──────────────────────────────────
// Pattern: parent { position: relative; } a { @include stretched-link(); }
@mixin cgui-stretched-link() {
	&::after {
		content: '';
		position: absolute;
		inset: 0;
		z-index: 1;
	}
}


// ─── Center absolute ────────────────────────────────────────────────────────
@mixin cgui-center-absolute($axis: both) {
	position: absolute;
	@if $axis == both {
		top: 50%; left: 50%;
		transform: translate(-50%, -50%);
	} @else if $axis == x {
		left: 50%;
		transform: translateX(-50%);
	} @else if $axis == y {
		top: 50%;
		transform: translateY(-50%);
	}
}


// ─── Card surface (běžné UI kontejnery) ─────────────────────────────────────
@mixin cgui-surface($padding: $cgui-space-4, $cgui-radius-arg: $cgui-radius, $shadow: $cgui-shadow-sm) {
	background: $cgui-color-bg;
	border: $cgui-border-width solid $cgui-color-border;
	border-radius: $cgui-radius-arg;
	padding: $padding;
	box-shadow: $shadow;
}
resources/sass/components/base/_animations.scss
// Sdílené keyframes pro CzG UI komponenty.

@keyframes czg-fade-in {
	from { opacity: 0; }
	to { opacity: 1; }
}

@keyframes czg-fade-out {
	from { opacity: 1; }
	to { opacity: 0; }
}

@keyframes czg-slide-down {
	from { opacity: 0; transform: translateY(-8px); }
	to { opacity: 1; transform: translateY(0); }
}

@keyframes czg-slide-up {
	from { opacity: 0; transform: translateY(8px); }
	to { opacity: 1; transform: translateY(0); }
}

@keyframes czg-slide-in-right {
	from { opacity: 0; transform: translateX(20px); }
	to { opacity: 1; transform: translateX(0); }
}
resources/js/components/base/utils.js
// Sdílené JS utility pro CzG UI komponenty.

export function $(selector, root = document) {
	return root.querySelector(selector);
}

export function $$(selector, root = document) {
	return Array.from(root.querySelectorAll(selector));
}

export function on(el, event, handler, opts) {
	el.addEventListener(event, handler, opts);
	return () => el.removeEventListener(event, handler, opts);
}

export function debounce(fn, wait = 100) {
	let t;
	return (...args) => {
		clearTimeout(t);
		t = setTimeout(() => fn(...args), wait);
	};
}

export function clamp(n, min, max) {
	return Math.max(min, Math.min(max, n));
}
resources/js/components/base/outside-click.js
// Helper pro detekci kliku mimo element. Používají overlay komponenty (dropdown, popover, ...).

const handlers = new Map();

/**
 * Zaregistruje callback který se zavolá při kliku mimo daný element.
 * Vrací unsubscribe funkci.
 *
 * @param {HTMLElement} el  — element, jehož "vnitřek" se ignoruje
 * @param {Function} cb     — callback při outside click
 * @returns {Function}      — unsubscribe
 */
export function onOutsideClick(el, cb) {
	const handler = (e) => {
		if (!el.contains(e.target)) {
			cb(e);
		}
	};

	// Registrace s mírným delayem, aby se nevyvolal click který komponentu otevřel.
	requestAnimationFrame(() => document.addEventListener('click', handler));

	const unsub = () => document.removeEventListener('click', handler);
	handlers.set(el, unsub);
	return unsub;
}

export function offOutsideClick(el) {
	const unsub = handlers.get(el);
	if (unsub) {
		unsub();
		handlers.delete(el);
	}
}